refactor(supply-api): declarify bootstrap server assembly
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
# Supply API Bootstrap Options Resolve Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 把 `supply-api/internal/app/bootstrap.go` 中剩余的参数校验和默认 rate-limit 装配收成更小的 helper,让 `BuildServer` 只保留声明式组装流程。
|
||||
|
||||
**Architecture:** 保留 `BuildServer` 现有外部接口和行为,新增 `resolveBuildServerOptions` 负责参数校验、环境解析和 server config 归一化,再新增 `resolveRateLimitConfig` 负责默认限流配置装配。`BuildServer` 最终只串联 `resolve -> buildRouteMux -> buildMiddlewareChain -> server` 四步。
|
||||
|
||||
**Tech Stack:** Go, Go test
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 提取 build server options resolve helper
|
||||
|
||||
**Files:**
|
||||
- Modify: `supply-api/internal/app/bootstrap.go`
|
||||
- Modify: `supply-api/internal/app/bootstrap_test.go`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
```go
|
||||
func TestResolveBuildServerOptions_RequiresAuthOutsideDev(t *testing.T) {
|
||||
_, err := resolveBuildServerOptions(BuildServerOptions{Env: "prod", ...})
|
||||
if err == nil {
|
||||
t.Fatal("expected auth middleware requirement")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd "supply-api" && go test ./internal/app -run 'TestResolveBuildServerOptions_RequiresAuthOutsideDev' -v`
|
||||
Expected: FAIL,因为 helper 尚不存在
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
```go
|
||||
type resolvedBuildServerOptions struct { ... }
|
||||
func resolveBuildServerOptions(opts BuildServerOptions) (resolvedBuildServerOptions, error) { ... }
|
||||
```
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cd "supply-api" && go test ./internal/app -run 'TestResolveBuildServerOptions_RequiresAuthOutsideDev' -v`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add supply-api/internal/app/bootstrap.go supply-api/internal/app/bootstrap_test.go
|
||||
git commit -m "refactor(supply-api): extract bootstrap options resolver"
|
||||
```
|
||||
|
||||
### Task 2: 提取默认 rate-limit helper
|
||||
|
||||
**Files:**
|
||||
- Modify: `supply-api/internal/app/bootstrap.go`
|
||||
- Modify: `supply-api/internal/app/bootstrap_test.go`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
```go
|
||||
func TestResolveRateLimitConfig_DefaultsDisabledInDev(t *testing.T) {
|
||||
cfg := resolveRateLimitConfig("dev", nil)
|
||||
if cfg.Enabled {
|
||||
t.Fatal("expected dev default to disable rate limit")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd "supply-api" && go test ./internal/app -run 'TestResolveRateLimitConfig_(DefaultsDisabledInDev|DefaultsEnabledOutsideDev)' -v`
|
||||
Expected: FAIL,因为 helper 尚不存在
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
```go
|
||||
func resolveRateLimitConfig(env string, cfg *middleware.RateLimitConfig) *middleware.RateLimitConfig { ... }
|
||||
```
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cd "supply-api" && go test ./internal/app -run 'TestResolveRateLimitConfig_(DefaultsDisabledInDev|DefaultsEnabledOutsideDev)' -v`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add supply-api/internal/app/bootstrap.go supply-api/internal/app/bootstrap_test.go
|
||||
git commit -m "refactor(supply-api): extract bootstrap rate limit resolver"
|
||||
```
|
||||
|
||||
### Task 3: 回归验证与收尾
|
||||
|
||||
**Files:**
|
||||
- Modify: `supply-api/internal/app/bootstrap.go`
|
||||
- Verify: `supply-api/internal/app/bootstrap_test.go`
|
||||
|
||||
**Step 1: Run focused tests**
|
||||
|
||||
Run: `cd "supply-api" && go test ./internal/app -run 'Test(ResolveBuildServerOptions_RequiresAuthOutsideDev|ResolveRateLimitConfig_(DefaultsDisabledInDev|DefaultsEnabledOutsideDev)|BuildServer_.*|BuildRouteMux_RegistersHealthAndSupplyRoutes|BuildMiddlewareChain_ProdRejectsQueryKey)' -v`
|
||||
Expected: PASS
|
||||
|
||||
**Step 2: Run package regression**
|
||||
|
||||
Run: `cd "supply-api" && go test ./internal/app ./cmd/supply-api ./internal/httpapi`
|
||||
Expected: PASS
|
||||
|
||||
**Step 3: Run e2e tests**
|
||||
|
||||
Run: `cd "supply-api" && go test -tags=e2e ./e2e`
|
||||
Expected: PASS
|
||||
|
||||
**Step 4: Run repo exit verification**
|
||||
|
||||
Run: `bash "scripts/ci/repo_integrity_check.sh"`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Check formatting**
|
||||
|
||||
Run: `git diff --check`
|
||||
Expected: no output
|
||||
|
||||
**Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/plans/2026-04-16-supply-api-bootstrap-options-resolve-plan.md supply-api/internal/app/bootstrap.go supply-api/internal/app/bootstrap_test.go
|
||||
git commit -m "refactor(supply-api): declarify bootstrap server assembly"
|
||||
```
|
||||
@@ -40,54 +40,77 @@ type middlewareChainOptions struct {
|
||||
RateLimitConfig *middleware.RateLimitConfig
|
||||
}
|
||||
|
||||
type resolvedBuildServerOptions struct {
|
||||
Env string
|
||||
ServerConfig config.ServerConfig
|
||||
Logger logging.Logger
|
||||
SupplyAPI *httpapi.SupplyAPI
|
||||
AlertAPI *httpapi.AlertAPI
|
||||
AuthMiddleware *middleware.AuthMiddleware
|
||||
RateLimitConfig *middleware.RateLimitConfig
|
||||
DBHealthCheck func(context.Context) error
|
||||
RedisHealthCheck func(context.Context) error
|
||||
}
|
||||
|
||||
// BuildServer 构建可复用的 HTTP server 与 handler 装配。
|
||||
func BuildServer(opts BuildServerOptions) (*http.Server, error) {
|
||||
resolved, err := resolveBuildServerOptions(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mux := buildRouteMux(buildRouteMuxOptions{
|
||||
SupplyAPI: resolved.SupplyAPI,
|
||||
AlertAPI: resolved.AlertAPI,
|
||||
DBHealthCheck: resolved.DBHealthCheck,
|
||||
RedisHealthCheck: resolved.RedisHealthCheck,
|
||||
})
|
||||
handler := buildMiddlewareChain(middlewareChainOptions{
|
||||
Env: resolved.Env,
|
||||
Logger: resolved.Logger,
|
||||
AuthMiddleware: resolved.AuthMiddleware,
|
||||
RateLimitConfig: resolved.RateLimitConfig,
|
||||
}, mux)
|
||||
|
||||
return &http.Server{
|
||||
Addr: resolved.ServerConfig.Addr,
|
||||
Handler: handler,
|
||||
ReadHeaderTimeout: resolved.ServerConfig.ReadTimeout,
|
||||
ReadTimeout: resolved.ServerConfig.ReadTimeout,
|
||||
WriteTimeout: resolved.ServerConfig.WriteTimeout,
|
||||
IdleTimeout: resolved.ServerConfig.IdleTimeout,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func resolveBuildServerOptions(opts BuildServerOptions) (resolvedBuildServerOptions, error) {
|
||||
if opts.SupplyAPI == nil {
|
||||
return nil, errors.New("supply api is required")
|
||||
return resolvedBuildServerOptions{}, errors.New("supply api is required")
|
||||
}
|
||||
if opts.AlertAPI == nil {
|
||||
return nil, errors.New("alert api is required")
|
||||
return resolvedBuildServerOptions{}, errors.New("alert api is required")
|
||||
}
|
||||
if opts.Logger == nil {
|
||||
return nil, errors.New("logger is required")
|
||||
return resolvedBuildServerOptions{}, errors.New("logger is required")
|
||||
}
|
||||
|
||||
env, err := ResolveEnv(opts.Env)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return resolvedBuildServerOptions{}, err
|
||||
}
|
||||
if env != "dev" && opts.AuthMiddleware == nil {
|
||||
return nil, errors.New("auth middleware is required outside dev")
|
||||
return resolvedBuildServerOptions{}, errors.New("auth middleware is required outside dev")
|
||||
}
|
||||
|
||||
rateLimitConfig := opts.RateLimitConfig
|
||||
if rateLimitConfig == nil {
|
||||
rateLimitConfig = middleware.DefaultRateLimitConfig()
|
||||
rateLimitConfig.Enabled = env != "dev"
|
||||
}
|
||||
|
||||
mux := buildRouteMux(buildRouteMuxOptions{
|
||||
return resolvedBuildServerOptions{
|
||||
Env: env,
|
||||
ServerConfig: normalizeServerConfig(opts.ServerConfig),
|
||||
Logger: opts.Logger,
|
||||
SupplyAPI: opts.SupplyAPI,
|
||||
AlertAPI: opts.AlertAPI,
|
||||
AuthMiddleware: opts.AuthMiddleware,
|
||||
RateLimitConfig: resolveRateLimitConfig(env, opts.RateLimitConfig),
|
||||
DBHealthCheck: opts.DBHealthCheck,
|
||||
RedisHealthCheck: opts.RedisHealthCheck,
|
||||
})
|
||||
handler := buildMiddlewareChain(middlewareChainOptions{
|
||||
Env: env,
|
||||
Logger: opts.Logger,
|
||||
AuthMiddleware: opts.AuthMiddleware,
|
||||
RateLimitConfig: rateLimitConfig,
|
||||
}, mux)
|
||||
|
||||
serverConfig := normalizeServerConfig(opts.ServerConfig)
|
||||
|
||||
return &http.Server{
|
||||
Addr: serverConfig.Addr,
|
||||
Handler: handler,
|
||||
ReadHeaderTimeout: serverConfig.ReadTimeout,
|
||||
ReadTimeout: serverConfig.ReadTimeout,
|
||||
WriteTimeout: serverConfig.WriteTimeout,
|
||||
IdleTimeout: serverConfig.IdleTimeout,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -110,6 +133,16 @@ func normalizeServerConfig(serverConfig config.ServerConfig) config.ServerConfig
|
||||
return serverConfig
|
||||
}
|
||||
|
||||
func resolveRateLimitConfig(env string, rateLimitConfig *middleware.RateLimitConfig) *middleware.RateLimitConfig {
|
||||
if rateLimitConfig != nil {
|
||||
return rateLimitConfig
|
||||
}
|
||||
|
||||
rateLimitConfig = middleware.DefaultRateLimitConfig()
|
||||
rateLimitConfig.Enabled = env != "dev"
|
||||
return rateLimitConfig
|
||||
}
|
||||
|
||||
func buildRouteMux(opts buildRouteMuxOptions) *http.ServeMux {
|
||||
mux := http.NewServeMux()
|
||||
healthHandler := httpapi.NewHealthHandlerWithDefaults(opts.DBHealthCheck, opts.RedisHealthCheck)
|
||||
|
||||
@@ -144,6 +144,43 @@ func TestBuildServer_DefaultsTimeoutsWhenUnset(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveBuildServerOptions_RequiresAuthOutsideDev(t *testing.T) {
|
||||
supplyAPI, alertAPI := mustBuildTestAPIs(t)
|
||||
|
||||
_, err := resolveBuildServerOptions(BuildServerOptions{
|
||||
Env: "prod",
|
||||
Logger: testLogger{},
|
||||
SupplyAPI: supplyAPI,
|
||||
AlertAPI: alertAPI,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected auth middleware requirement outside dev")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "auth middleware") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveRateLimitConfig_DefaultsDisabledInDev(t *testing.T) {
|
||||
cfg := resolveRateLimitConfig("dev", nil)
|
||||
if cfg == nil {
|
||||
t.Fatal("expected rate limit config")
|
||||
}
|
||||
if cfg.Enabled {
|
||||
t.Fatal("expected dev default rate limit to be disabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveRateLimitConfig_DefaultsEnabledOutsideDev(t *testing.T) {
|
||||
cfg := resolveRateLimitConfig("prod", nil)
|
||||
if cfg == nil {
|
||||
t.Fatal("expected rate limit config")
|
||||
}
|
||||
if !cfg.Enabled {
|
||||
t.Fatal("expected non-dev default rate limit to be enabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRouteMux_RegistersHealthAndSupplyRoutes(t *testing.T) {
|
||||
supplyAPI, alertAPI := mustBuildTestAPIs(t)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user