# 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 {
    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_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_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)
