# Data Model: Fiber Middleware Integration **Feature**: 001-fiber-middleware-integration **Date**: 2025-11-10 **Phase**: 1 - Design & Contracts ## Overview This document defines the data structures, entities, and models used in the middleware integration feature. All structures follow Go idiomatic design principles with simple, flat structures and no Java-style patterns. --- ## 1. Configuration Model ### Config Structure **File**: `pkg/config/config.go` ```go // Config represents the application configuration type Config struct { Server ServerConfig `mapstructure:"server"` Redis RedisConfig `mapstructure:"redis"` Logging LoggingConfig `mapstructure:"logging"` Middleware MiddlewareConfig `mapstructure:"middleware"` } // ServerConfig contains HTTP server settings type ServerConfig struct { Address string `mapstructure:"address"` // e.g., ":3000" ReadTimeout time.Duration `mapstructure:"read_timeout"` // e.g., "10s" WriteTimeout time.Duration `mapstructure:"write_timeout"` // e.g., "10s" ShutdownTimeout time.Duration `mapstructure:"shutdown_timeout"` // e.g., "30s" Prefork bool `mapstructure:"prefork"` // Multi-process mode } // RedisConfig contains Redis connection settings type RedisConfig struct { Address string `mapstructure:"address"` // e.g., "localhost:6379" Password string `mapstructure:"password"` // Leave empty if no auth DB int `mapstructure:"db"` // Database number (0-15) PoolSize int `mapstructure:"pool_size"` // Max connections MinIdleConns int `mapstructure:"min_idle_conns"` // Keep-alive connections DialTimeout time.Duration `mapstructure:"dial_timeout"` // e.g., "5s" ReadTimeout time.Duration `mapstructure:"read_timeout"` // e.g., "3s" WriteTimeout time.Duration `mapstructure:"write_timeout"` // e.g., "3s" } // LoggingConfig contains logging settings type LoggingConfig struct { Level string `mapstructure:"level"` // debug, info, warn, error Development bool `mapstructure:"development"` // Enable dev mode (pretty print) AppLog LogRotationConfig `mapstructure:"app_log"` // Application log settings AccessLog LogRotationConfig `mapstructure:"access_log"` // HTTP access log settings } // LogRotationConfig contains log rotation settings for Lumberjack type LogRotationConfig struct { Filename string `mapstructure:"filename"` // Log file path MaxSize int `mapstructure:"max_size"` // Max size in MB before rotation MaxBackups int `mapstructure:"max_backups"` // Max number of old files to keep MaxAge int `mapstructure:"max_age"` // Max days to retain old files Compress bool `mapstructure:"compress"` // Compress rotated files } // MiddlewareConfig contains middleware settings type MiddlewareConfig struct { EnableAuth bool `mapstructure:"enable_auth"` // Enable keyauth middleware EnableRateLimiter bool `mapstructure:"enable_rate_limiter"` // Enable limiter (default: false) RateLimiter RateLimiterConfig `mapstructure:"rate_limiter"` // Rate limiter settings } // RateLimiterConfig contains rate limiter settings type RateLimiterConfig struct { Max int `mapstructure:"max"` // Max requests per window Expiration time.Duration `mapstructure:"expiration"` // Time window (e.g., "1m") Storage string `mapstructure:"storage"` // "memory" or "redis" } // Validate validates configuration values func (c *Config) Validate() error { if c.Server.Address == "" { return errors.New("server address cannot be empty") } if c.Server.ReadTimeout <= 0 { return errors.New("server read timeout must be positive") } if c.Redis.Address == "" { return errors.New("redis address cannot be empty") } if c.Redis.PoolSize <= 0 { return errors.New("redis pool size must be positive") } if c.Logging.AppLog.Filename == "" { return errors.New("app log filename cannot be empty") } if c.Logging.AccessLog.Filename == "" { return errors.New("access log filename cannot be empty") } if c.Middleware.RateLimiter.Max <= 0 { c.Middleware.RateLimiter.Max = 100 // Default } return nil } ``` **Example YAML Configuration** (`configs/config.yaml`): ```yaml server: address: ":3000" read_timeout: "10s" write_timeout: "10s" shutdown_timeout: "30s" prefork: false redis: address: "localhost:6379" password: "" db: 0 pool_size: 10 min_idle_conns: 5 dial_timeout: "5s" read_timeout: "3s" write_timeout: "3s" logging: level: "info" development: false app_log: filename: "logs/app.log" max_size: 100 # MB max_backups: 30 max_age: 30 # days compress: true access_log: filename: "logs/access.log" max_size: 500 # MB max_backups: 90 max_age: 90 # days compress: true middleware: enable_auth: true enable_rate_limiter: false # Disabled by default rate_limiter: max: 100 # requests expiration: "1m" # per minute storage: "memory" # or "redis" ``` **Validation Rules**: - All required fields must be non-empty - Timeouts must be positive durations - Pool sizes must be positive integers - Log filenames must be valid paths - Rate limiter max must be positive (defaults to 100) --- ## 2. Authentication Model ### AuthToken Entity **Storage**: Redis key-value pair **Key Format**: `auth:token:{token_string}` (generated via `constants.RedisAuthTokenKey()`) **Value Format**: Plain string containing user ID **TTL**: Managed by Redis (set when token is created) ```go // Token validation doesn't use a struct - just Redis key-value // Key: "auth:token:abc123def456" // Value: "user-789" // TTL: 3600 seconds (1 hour) ``` **Redis Operations**: ```go // Store token (example - not part of this feature) rdb.Set(ctx, constants.RedisAuthTokenKey(token), userID, 1*time.Hour) // Validate token (this feature) userID, err := rdb.Get(ctx, constants.RedisAuthTokenKey(token)).Result() if err == redis.Nil { // Token not found or expired } // Delete token (example - logout feature) rdb.Del(ctx, constants.RedisAuthTokenKey(token)) ``` **Key Generation Function** (`pkg/constants/redis.go`): ```go // RedisAuthTokenKey generates Redis key for authentication tokens func RedisAuthTokenKey(token string) string { return fmt.Sprintf("auth:token:%s", token) } ``` **Validation Rules**: - Token must exist as Redis key - Redis must be available (fail closed if not) - Token value must be non-empty user ID - TTL is checked automatically by Redis (expired keys return `redis.Nil`) --- ## 3. Request Context Model ### RequestContext Structure **File**: `pkg/middleware/context.go` (or stored in Fiber's `c.Locals()`) ```go // Request context is stored in Fiber's Locals, not a struct // Access via: c.Locals("key") // Request ID (set by requestid middleware) requestID := c.Locals(constants.ContextKeyRequestID).(string) // UUID v4 string // User ID (set by keyauth middleware after validation) userID, ok := c.Locals(constants.ContextKeyUserID).(string) if !ok { // Not authenticated } // Start time (for duration calculation) startTime := c.Locals(constants.ContextKeyStartTime).(time.Time) ``` **Context Keys** (constants in `pkg/constants/constants.go`): ```go const ( // Context keys for Fiber Locals ContextKeyRequestID = "requestid" ContextKeyUserID = "user_id" ContextKeyStartTime = "start_time" ) ``` **Lifecycle**: 1. **requestid middleware**: Sets `requestid` in Locals (UUID v4) 2. **logger middleware**: Sets `start_time` in Locals 3. **keyauth middleware**: Sets `user_id` in Locals (after validation) 4. **handler**: Accesses context values from Locals 5. **logger middleware** (after handler): Calculates duration and logs --- ## 4. Log Entry Models ### Application Log Entry **Format**: JSON **Output**: `logs/app.log` **Logger**: Zap (appLogger instance) ```json { "timestamp": "2025-11-10T15:30:45.123Z", "level": "info", "logger": "service.user", "caller": "user/service.go:42", "message": "User created successfully", "request_id": "550e8400-e29b-41d4-a716-446655440000", "user_id": "user-789", "username": "john_doe", "ip": "192.168.1.100" } ``` **Fields**: - `timestamp`: ISO 8601 format (RFC3339) - `level`: debug, info, warn, error - `logger`: Logger name (optional, for structured logging) - `caller`: Source file and line number - `message`: Log message - `request_id`: Request correlation ID (if available) - `user_id`: Authenticated user ID (if available) - Custom fields: Any additional context-specific fields **Zap Usage**: ```go appLogger.Info("User created successfully", zap.String("request_id", requestID), zap.String(constants.ContextKeyUserID, userID), zap.String("username", username), zap.String("ip", ip), ) ``` ### Access Log Entry Format **Purpose**: Records all HTTP requests for audit trail, performance monitoring, and troubleshooting **Format**: JSON (one entry per line) **Output**: `logs/access.log` **Logger**: Zap (accessLogger instance) **Requirement**: Implements spec.md FR-011 **Complete JSON Schema**: ```json { "timestamp": "2025-11-10T15:30:45.123Z", "level": "info", "method": "POST", "path": "/api/v1/users", "status": 200, "duration_ms": 45.234, "request_id": "550e8400-e29b-41d4-a716-446655440000", "ip": "192.168.1.100", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)...", "user_id": "user-789" } ``` **Field Definitions**: | Field | Type | Required | Description | Example | |-------|------|----------|-------------|---------| | `timestamp` | string | Yes | ISO 8601 timestamp (RFC3339 with milliseconds) | `2025-11-10T15:30:45.123Z` | | `level` | string | Yes | Log level (always "info" for access logs) | `info` | | `method` | string | Yes | HTTP method | `GET`, `POST`, `PUT`, `DELETE`, `PATCH` | | `path` | string | Yes | Request path (including query params) | `/api/v1/users?page=1` | | `status` | int | Yes | HTTP status code | `200`, `401`, `500` | | `duration_ms` | float64 | Yes | Request processing duration in milliseconds | `45.234` | | `request_id` | string | Yes | UUID v4 request identifier | `550e8400-e29b-41d4-a716-446655440000` | | `ip` | string | Yes | Client IP address | `192.168.1.100` | | `user_agent` | string | Yes | User-Agent header value | `Mozilla/5.0...` | | `user_id` | string | No | Authenticated user ID (empty string if not authenticated) | `user-789` or `""` | **Notes**: - All access logs are written at "info" level - `user_id` field is empty string (`""`) for unauthenticated requests - `duration_ms` includes full middleware chain execution time - `path` includes query parameters for complete request tracking - Logged after response is sent (includes actual status code) **Zap Usage**: ```go accessLogger.Info("", zap.String("method", c.Method()), zap.String("path", c.Path()), zap.Int("status", c.Response().StatusCode()), zap.Float64("duration_ms", duration.Seconds()*1000), zap.String("request_id", requestID), zap.String("ip", c.IP()), zap.String("user_agent", c.Get("User-Agent")), zap.String(constants.ContextKeyUserID, userID), ) ``` --- ## 5. API Response Model ### Unified Response Structure **File**: `pkg/response/response.go` ```go // Response is the unified API response structure type Response struct { Code int `json:"code"` // Application error code (0 = success) Data interface{} `json:"data"` // Response data (object, array, or null) Message string `json:"msg"` // Human-readable message Timestamp string `json:"timestamp"` // ISO 8601 timestamp (optional, can be added) } // Success returns a successful response func Success(c *fiber.Ctx, data interface{}) error { return c.JSON(Response{ Code: 0, Data: data, Message: "success", Timestamp: time.Now().Format(time.RFC3339), }) } // Error returns an error response func Error(c *fiber.Ctx, httpStatus int, code int, message string) error { return c.Status(httpStatus).JSON(Response{ Code: code, Data: nil, Message: message, Timestamp: time.Now().Format(time.RFC3339), }) } // SuccessWithMessage returns a successful response with custom message func SuccessWithMessage(c *fiber.Ctx, data interface{}, message string) error { return c.JSON(Response{ Code: 0, Data: data, Message: message, Timestamp: time.Now().Format(time.RFC3339), }) } ``` **Success Response Example**: ```json { "code": 0, "data": { "id": "user-123", "name": "John Doe", "email": "john@example.com" }, "msg": "success", "timestamp": "2025-11-10T15:30:45Z" } ``` **Error Response Example**: ```json { "code": 1002, "data": null, "msg": "Invalid or expired token", "timestamp": "2025-11-10T15:30:45Z" } ``` **List Response Example**: ```json { "code": 0, "data": [ {"id": "1", "name": "Item 1"}, {"id": "2", "name": "Item 2"} ], "msg": "success", "timestamp": "2025-11-10T15:30:45Z" } ``` --- ## 6. Error Model ### Error Code Constants **File**: `pkg/errors/codes.go` ```go // Application error codes const ( CodeSuccess = 0 // Success CodeInternalError = 1000 // Internal server error CodeMissingToken = 1001 // Missing authentication token CodeInvalidToken = 1002 // Invalid or expired token CodeTooManyRequests = 1003 // Too many requests (rate limited) CodeAuthServiceUnavailable = 1004 // Authentication service unavailable (Redis down) ) // Error messages (bilingual) var errorMessages = map[int]struct { EN string ZH string }{ CodeSuccess: {"Success", "成功"}, CodeInternalError: {"Internal server error", "内部服务器错误"}, CodeMissingToken: {"Missing authentication token", "缺失认证令牌"}, CodeInvalidToken: {"Invalid or expired token", "令牌无效或已过期"}, CodeTooManyRequests: {"Too many requests", "请求过于频繁"}, CodeAuthServiceUnavailable: {"Authentication service unavailable", "认证服务不可用"}, } // GetMessage returns error message for given code and language func GetMessage(code int, lang string) string { msg, ok := errorMessages[code] if !ok { return "Unknown error" } if lang == "zh" { return msg.ZH } return msg.EN } ``` ### Custom Error Types **File**: `pkg/errors/errors.go` ```go import "errors" // Standard error types for middleware var ( ErrMissingToken = errors.New("missing authentication token") ErrInvalidToken = errors.New("invalid or expired token") ErrRedisUnavailable = errors.New("redis unavailable") ErrTooManyRequests = errors.New("too many requests") ) // AppError represents an application error with code type AppError struct { Code int // Application error code Message string // Error message Err error // Underlying error (optional) } func (e *AppError) Error() string { if e.Err != nil { return fmt.Sprintf("%s: %v", e.Message, e.Err) } return e.Message } func (e *AppError) Unwrap() error { return e.Err } // New creates a new AppError func New(code int, message string) *AppError { return &AppError{ Code: code, Message: message, } } // Wrap wraps an existing error with code and message func Wrap(code int, message string, err error) *AppError { return &AppError{ Code: code, Message: message, Err: err, } } ``` --- ## 7. Rate Limit State Model ### Rate Limit Tracking **Storage**: In-memory (default) or Redis (for distributed) **Managed by**: Fiber limiter middleware (internal storage) ```go // Rate limit state is managed internally by Fiber limiter // For memory storage: map[string]*limiterEntry // For Redis storage: Redis keys with TTL // Memory storage structure (internal to Fiber) type limiterEntry struct { count int // Current request count expiration time.Time // Window expiration time } // Redis storage structure (if using Redis storage) // Key: "ratelimit:{ip_address}" // Value: JSON {"count": 5, "expiration": "2025-11-10T15:31:00Z"} // TTL: Same as expiration window ``` **Key Generation** (for custom Redis storage): ```go // pkg/constants/redis.go func RedisRateLimitKey(ip string) string { return fmt.Sprintf("ratelimit:%s", ip) } ``` **Access Pattern**: - Middleware checks rate limit state before handler - Increment counter on each request - Reset counter when window expires - Return 429 when limit exceeded --- ## Entity Relationship Diagram ``` ┌─────────────────┐ │ Configuration │ (YAML file) │ - Server │ │ - Redis │ │ - Logging │ │ - Middleware │ └────────┬────────┘ │ loads into ▼ ┌─────────────────┐ ┌──────────────────┐ │ Application │────▶│ Redis Client │ │ (main.go) │ │ (connection │ └────────┬────────┘ │ pool) │ │ └──────────┬───────┘ │ creates │ ▼ │ validates tokens ┌─────────────────┐ │ │ Middleware │ │ │ Chain: │ │ │ - Recover │ │ │ - RequestID │ │ │ - Logger │ │ │ - KeyAuth │◀───────────────┘ │ - RateLimiter │ └────────┬────────┘ │ processes ▼ ┌─────────────────┐ ┌──────────────────┐ │ Handlers │────▶│ Response │ │ (API │ │ {code, data, │ │ endpoints) │ │ msg} │ └─────────────────┘ └──────────────────┘ │ │ logs to ▼ ┌─────────────────┐ │ Log Files │ │ - app.log │ (Zap + Lumberjack) │ - access.log │ └─────────────────┘ ``` --- ## Data Flow Diagram ### Request Processing Flow ``` 1. HTTP Request arrives ↓ 2. Recover Middleware (catch panics) ↓ 3. RequestID Middleware (generate UUID v4) → Store in c.Locals(constants.ContextKeyRequestID) ↓ 4. Logger Middleware (start) → Store start_time in c.Locals(constants.ContextKeyStartTime) ↓ 5. KeyAuth Middleware → Extract token from header → Call TokenValidator.Validate(token) → Validate with Redis: GET auth:token:{token} → If valid: Store user_id in c.Locals(constants.ContextKeyUserID) → If invalid: Return 401 with error code → If Redis down: Return 503 with error code ↓ 6. [RateLimiter Middleware] (if enabled) → Check rate limit for c.IP() → If exceeded: Return 429 with error code ↓ 7. Handler (business logic) → Access c.Locals(constants.ContextKeyRequestID), c.Locals(constants.ContextKeyUserID) → Process request → Return response via response.Success() or response.Error() ↓ 8. Logger Middleware (end) → Calculate duration → Log to access.log with all context ↓ 9. HTTP Response sent ``` --- ## Summary **Key Entities**: 1. **Config**: Application configuration (YAML → struct) 2. **AuthToken**: Redis key-value (token → user ID) 3. **RequestContext**: Fiber Locals (requestid, user_id, start_time) 4. **LogEntry**: JSON logs (app.log, access.log) 5. **Response**: Unified API response ({code, data, msg}) 6. **Error**: Error codes and custom types 7. **RateLimitState**: Managed by Fiber limiter (memory or Redis) **Design Principles**: - ✅ Simple, flat structures (no deep nesting) - ✅ Direct field access (no getters/setters) - ✅ Composition over inheritance - ✅ Explicit error handling - ✅ Go naming conventions (URL, ID, HTTP) - ✅ No Java-style patterns (no I-prefix, no Impl-suffix) **Next**: Generate API contracts (OpenAPI specification) --- ## 8. Configuration Validation Rules This section defines comprehensive validation constraints for all configuration fields, implementing the requirements in spec.md FR-003. ### Validation Error Format All validation errors MUST follow this format: ``` "Invalid configuration: {field_path}: {error_reason} (current value: {value}, expected: {constraint})" ``` Example: ``` "Invalid configuration: server.read_timeout: duration out of range (current value: 1s, expected: 5s-300s)" ``` --- ### Server Configuration Validation | Field | Type | Required | Constraint | Default | Error Message | |-------|------|----------|------------|---------|---------------| | `server.address` | string | Yes | Non-empty, format `:PORT` or `HOST:PORT` | `:3000` | "server.address: must be non-empty and in format ':PORT' or 'HOST:PORT'" | | `server.read_timeout` | duration | Yes | 5s - 300s | `10s` | "server.read_timeout: duration out of range (expected: 5s-300s)" | | `server.write_timeout` | duration | Yes | 5s - 300s | `10s` | "server.write_timeout: duration out of range (expected: 5s-300s)" | | `server.shutdown_timeout` | duration | Yes | 10s - 120s | `30s` | "server.shutdown_timeout: duration out of range (expected: 10s-120s)" | | `server.prefork` | bool | No | true or false | `false` | "server.prefork: must be boolean (true/false)" | --- ### Redis Configuration Validation | Field | Type | Required | Constraint | Default | Error Message | |-------|------|----------|------------|---------|---------------| | `redis.address` | string | Yes | Non-empty, format `HOST:PORT` | `localhost:6379` | "redis.address: must be non-empty and in format 'HOST:PORT'" | | `redis.password` | string | No | Any string (empty allowed) | `""` | N/A | | `redis.db` | int | No | 0 - 15 | `0` | "redis.db: database number out of range (expected: 0-15)" | | `redis.pool_size` | int | No | 1 - 1000 | `10` | "redis.pool_size: pool size out of range (expected: 1-1000)" | | `redis.min_idle_conns` | int | No | 0 - pool_size | `5` | "redis.min_idle_conns: must be 0 to pool_size" | | `redis.dial_timeout` | duration | No | 1s - 30s | `5s` | "redis.dial_timeout: timeout out of range (expected: 1s-30s)" | | `redis.read_timeout` | duration | No | 1s - 30s | `3s` | "redis.read_timeout: timeout out of range (expected: 1s-30s)" | | `redis.write_timeout` | duration | No | 1s - 30s | `3s` | "redis.write_timeout: timeout out of range (expected: 1s-30s)" | --- ### Logging Configuration Validation | Field | Type | Required | Constraint | Default | Error Message | |-------|------|----------|------------|---------|---------------| | `logging.level` | string | No | One of: debug, info, warn, error | `info` | "logging.level: invalid log level (expected: debug, info, warn, error)" | | `logging.development` | bool | No | true or false | `false` | "logging.development: must be boolean (true/false)" | #### App Log Validation | Field | Type | Required | Constraint | Default | Error Message | |-------|------|----------|------------|---------|---------------| | `logging.app_log.filename` | string | Yes | Non-empty, valid file path | `logs/app.log` | "logging.app_log.filename: must be non-empty valid file path" | | `logging.app_log.max_size` | int | No | 1 - 1000 (MB) | `100` | "logging.app_log.max_size: size out of range (expected: 1-1000 MB)" | | `logging.app_log.max_backups` | int | No | 0 - 999 (0 = keep all) | `30` | "logging.app_log.max_backups: count out of range (expected: 0-999)" | | `logging.app_log.max_age` | int | No | 1 - 365 (days) | `30` | "logging.app_log.max_age: retention period out of range (expected: 1-365 days)" | | `logging.app_log.compress` | bool | No | true or false | `true` | "logging.app_log.compress: must be boolean (true/false)" | #### Access Log Validation | Field | Type | Required | Constraint | Default | Error Message | |-------|------|----------|------------|---------|---------------| | `logging.access_log.filename` | string | Yes | Non-empty, valid file path | `logs/access.log` | "logging.access_log.filename: must be non-empty valid file path" | | `logging.access_log.max_size` | int | No | 1 - 1000 (MB) | `500` | "logging.access_log.max_size: size out of range (expected: 1-1000 MB)" | | `logging.access_log.max_backups` | int | No | 0 - 999 (0 = keep all) | `90` | "logging.access_log.max_backups: count out of range (expected: 0-999)" | | `logging.access_log.max_age` | int | No | 1 - 365 (days) | `90` | "logging.access_log.max_age: retention period out of range (expected: 1-365 days)" | | `logging.access_log.compress` | bool | No | true or false | `true` | "logging.access_log.compress: must be boolean (true/false)" | --- ### Middleware Configuration Validation | Field | Type | Required | Constraint | Default | Error Message | |-------|------|----------|------------|---------|---------------| | `middleware.enable_auth` | bool | No | true or false | `true` | "middleware.enable_auth: must be boolean (true/false)" | | `middleware.enable_rate_limiter` | bool | No | true or false | `false` | "middleware.enable_rate_limiter: must be boolean (true/false)" | #### Rate Limiter Validation | Field | Type | Required | Constraint | Default | Error Message | |-------|------|----------|------------|---------|---------------| | `middleware.rate_limiter.max` | int | No | 1 - 10000 | `30` | "middleware.rate_limiter.max: request limit out of range (expected: 1-10000)" | | `middleware.rate_limiter.expiration` | duration | No | 1s - 1h | `1m` | "middleware.rate_limiter.expiration: window duration out of range (expected: 1s-1h)" | | `middleware.rate_limiter.storage` | string | No | "memory" or "redis" | `memory` | "middleware.rate_limiter.storage: invalid storage type (expected: memory or redis)" | --- ### Validation Implementation Guidelines **Location**: `pkg/config/loader.go` - `Validate()` function **Validation Order**: 1. **Required fields check** - Fail fast if critical fields missing 2. **Type validation** - Viper handles basic type conversion 3. **Range validation** - Check numeric bounds 4. **Format validation** - Validate string formats (HOST:PORT, file paths) 5. **Cross-field validation** - Check dependencies (e.g., min_idle_conns <= pool_size) **Example Implementation Pattern**: ```go func (c *Config) Validate() error { // Required fields if c.Server.Address == "" { return fmt.Errorf("Invalid configuration: server.address: must be non-empty (current value: empty)") } // Range validation if c.Server.ReadTimeout < 5*time.Second || c.Server.ReadTimeout > 300*time.Second { return fmt.Errorf("Invalid configuration: server.read_timeout: duration out of range (current value: %s, expected: 5s-300s)", c.Server.ReadTimeout) } // Format validation if !strings.Contains(c.Redis.Address, ":") { return fmt.Errorf("Invalid configuration: redis.address: invalid format (current value: %s, expected: HOST:PORT)", c.Redis.Address) } // Cross-field validation if c.Redis.MinIdleConns > c.Redis.PoolSize { return fmt.Errorf("Invalid configuration: redis.min_idle_conns: must not exceed pool_size (current value: %d, pool_size: %d)", c.Redis.MinIdleConns, c.Redis.PoolSize) } return nil } ``` **Testing Requirements**: - Unit tests MUST cover all validation rules (T020 in tasks.md) - Test valid configurations (should pass) - Test each constraint violation (should fail with correct error message) - Test edge cases (min/max boundaries) - Test malformed YAML (should be caught by Viper)