Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# AGENTS.md

This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.

## Commands

### Build and Development
```bash
# Run all tests
go test ./...

# Run tests with verbose output
go test -v ./...

# Run tests with coverage
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out

# Run a single test
go test -run TestName ./pkg/router

# Run tests for a specific package
go test ./pkg/router/...

# Build examples
cd examples/simple && go build
```

### Code Quality
```bash
# Format code
go fmt ./...

# Run Go vet
go vet ./...

# Install and run golangci-lint (if needed)
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
golangci-lint run
```

## Architecture Overview

SRouter is a high-performance HTTP router framework built on `julienschmidt/httprouter` with Go generics support (requires Go 1.24.0+). The codebase follows a layered architecture with clear separation of concerns.

### Core Type Parameters
Throughout the codebase, `T` represents the UserID type (must be comparable) and `U` represents the User object type. These generic parameters enable type-safe authentication and context management.

### Package Structure
- **pkg/router/**: Core routing engine with generic route registration, middleware orchestration, and request handling
- **pkg/middleware/**: Authentication providers, rate limiting, tracing, and database transaction middleware
- **pkg/codec/**: Request/response marshaling interfaces and implementations (JSON, Protocol Buffers)
- **pkg/metrics/**: Interface-based metrics system for pluggable backends
- **pkg/scontext/**: Centralized context management with SRouterContext[T,U] wrapper
- **pkg/common/**: Shared types like Middleware, RateLimitConfig

### Request Flow
1. HTTP Request → Router.ServeHTTP
2. CORS handling (if configured)
3. Client IP extraction based on IPConfig
4. Metrics and trace ID injection
5. httprouter path matching
6. Middleware chain execution (Recovery → Auth → RateLimit → Route-specific → Global → Timeout → Handler)
7. Generic handler marshaling/unmarshaling (if applicable)

### Key Design Patterns
- **Middleware Chain**: Composable middleware with configurable execution order
- **Configuration Hierarchy**: Global → SubRouter → Route with cascading overrides
- **Generic Routes**: Type-safe handlers with automatic codec-based marshaling
- **Context Wrapper**: Single SRouterContext avoids deep context nesting

### Testing Approach
The codebase maintains >90% coverage with comprehensive unit tests. Tests often use generic test helpers and mock interfaces (e.g., in router/internal/mocks/).

## Important Concepts

### Authentication Levels
Routes support three authentication levels:
- `NoAuth`: No authentication required
- `AuthOptional`: Authentication attempted but not required
- `AuthRequired`: Authentication mandatory

Authentication is typically handled by middleware that populates the context using scontext helpers.

### Rate Limiting
Flexible rate limiting with strategies:
- `StrategyIP`: Based on client IP
- `StrategyUser`: Based on authenticated user ID
- `StrategyCustom`: Custom key extraction

Uses Uber's ratelimit library with leaky bucket algorithm.

### Generic Route Registration
Generic routes should be registered using `NewGenericRouteDefinition` within SubRouterConfig.Routes for declarative configuration:
```go
router.NewGenericRouteDefinition[ReqType, RespType, UserIDType, UserType](
router.RouteConfig[ReqType, RespType]{...}
)
```

### Context Access
Always use scontext package helpers for type-safe context access:
```go
userID, ok := scontext.GetUserIDFromRequest[T, U](r)
user, ok := scontext.GetUserFromRequest[T, U](r) // Returns *U
traceID := scontext.GetTraceIDFromRequest[T, U](r)
handlerErr, ok := scontext.GetHandlerErrorFromRequest[T, U](r) // For generic routes
```

### Handler Error Context
Generic routes automatically store handler errors in the request context, allowing middleware to access them after handler execution. This is useful for:
- Transaction rollback decisions
- Custom error logging
- Circuit breaker patterns
- Error metrics collection

### Trace ID Generation
Enable trace ID generation by setting `TraceIDBufferSize > 0` in RouterConfig. This creates a background ID generator for efficient UUID generation and automatic request correlation.
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ apiV1SubRouter := router.SubRouterConfig{
router.RouteConfig[CreateUserReq, CreateUserResp]{
Path: "/users", // Path relative to the sub-router prefix (/api/v1/users)
Methods: []router.HttpMethod{router.MethodPost},
AuthLevel: router.Ptr(router.AuthRequired),
AuthLevel: new(router.AuthRequired),
Codec: codec.NewJSONCodec[CreateUserReq, CreateUserResp](),
Handler: CreateUserHandler,
// Middlewares, Timeout, MaxBodySize, RateLimit can be set here too
Expand Down Expand Up @@ -291,7 +291,7 @@ router.RegisterGenericRoute[CreateUserReq, CreateUserResp, string, string](r,
router.RouteConfig[CreateUserReq, CreateUserResp]{
Path: "/standalone/users",
Methods: []router.HttpMethod{router.MethodPost},
AuthLevel: router.Ptr(router.AuthRequired), // Use Ptr helper
AuthLevel: new(router.AuthRequired), // Use Ptr helper
Codec: codec.NewJSONCodec[CreateUserReq, CreateUserResp](),
Handler: CreateUserHandler,
},
Expand Down Expand Up @@ -700,9 +700,9 @@ When using the built-in `AuthOptional`/`AuthRequired` middleware, the token is e

```go
// Example route configurations
routePublic := router.RouteConfigBase{ AuthLevel: router.Ptr(router.NoAuth), ... }
routeOptional := router.RouteConfigBase{ AuthLevel: router.Ptr(router.AuthOptional), ... }
routeProtected := router.RouteConfigBase{ AuthLevel: router.Ptr(router.AuthRequired), ... }
routePublic := router.RouteConfigBase{ AuthLevel: new(router.NoAuth), ... }
routeOptional := router.RouteConfigBase{ AuthLevel: new(router.AuthOptional), ... }
routeProtected := router.RouteConfigBase{ AuthLevel: new(router.AuthRequired), ... }
```

#### Authentication Middleware
Expand Down Expand Up @@ -1060,7 +1060,7 @@ xmlRouteDef := router.NewGenericRouteDefinition[CreateUserReq, CreateUserResp, s
router.RouteConfig[CreateUserReq, CreateUserResp]{
Path: "/api/users",
Methods: []router.HttpMethod{router.MethodPost},
AuthLevel: router.Ptr(router.NoAuth), // Use Ptr helper
AuthLevel: new(router.NoAuth), // Use Ptr helper
Codec: NewXMLCodec[CreateUserReq, CreateUserResp](),
Handler: CreateUserHandler, // Assume handler exists
},
Expand Down
10 changes: 5 additions & 5 deletions docs/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,23 @@ routePublic := router.RouteConfigBase{
Path: "/public/info",
// AuthLevel: nil, // Defaults to NoAuth
// Or explicitly:
AuthLevel: router.Ptr(router.NoAuth),
AuthLevel: new(router.NoAuth),
// ... handler, methods
}

routeOptional := router.RouteConfigBase{
Path: "/user/profile", // Maybe shows generic profile if not logged in, specific if logged in
AuthLevel: router.Ptr(router.AuthOptional),
AuthLevel: new(router.AuthOptional),
// ... handler, methods
}

routeProtected := router.RouteConfig[UpdateSettingsReq, UpdateSettingsResp]{
Path: "/user/settings",
AuthLevel: router.Ptr(router.AuthRequired), // Must be logged in
AuthLevel: new(router.AuthRequired), // Must be logged in
// ... handler, methods, codec
}
```
*(Note: `router.Ptr()` is a simple helper function to get a pointer to an `AuthLevel` value, as the config fields expect pointers)*
*(Note: `new()` is a simple helper function to get a pointer to an `AuthLevel` value, as the config fields expect pointers)*

## Authentication Functions (`NewRouter`)

Expand Down Expand Up @@ -140,7 +140,7 @@ dummyGetIDFunc := func(user *MyUserType) string { return "" }
// UserIDType and UserObjectType for NewRouter must match what MyApiKeyMiddleware puts in context
r := router.NewRouter[string, MyUserType](routerConfig, dummyAuthFunc, dummyGetIDFunc)

// Routes using this custom middleware might set AuthLevel: router.Ptr(router.NoAuth)
// Routes using this custom middleware might set AuthLevel: new(router.NoAuth)
// if MyApiKeyMiddleware handles all required/optional logic itself.
```

Expand Down
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ const (
)

// Ptr returns a pointer to an AuthLevel value (helper for config).
func Ptr(level AuthLevel) *AuthLevel {
funcnew(level AuthLevel) *AuthLevel {
return &level
}
```
Expand Down
2 changes: 1 addition & 1 deletion docs/generic-routes.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ type CreateUserResp struct {
createUserRoute := router.RouteConfig[CreateUserReq, CreateUserResp]{
Path: "/users",
Methods: []router.HttpMethod{router.MethodPost},
AuthLevel: router.Ptr(router.AuthRequired), // Example: Requires authentication
AuthLevel: new(router.AuthRequired), // Example: Requires authentication
Codec: codec.NewJSONCodec[CreateUserReq, CreateUserResp](), // Specify the codec
Handler: CreateUserHandler, // Assign the generic handler
// Optional overrides for timeout, body size, or rate limit
Expand Down
2 changes: 1 addition & 1 deletion docs/middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ routerConfig := router.RouterConfig{
mymiddleware.LogUserIDMiddleware(logger), // Route: Runs last before handler
},
Handler: GetUsersHandler,
AuthLevel: router.Ptr(router.AuthRequired), // Example: Requires authentication
AuthLevel: new(router.AuthRequired), // Example: Requires authentication
},
// ... other v1 routes
},
Expand Down
6 changes: 3 additions & 3 deletions docs/routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ apiV1SubRouter := router.SubRouterConfig{
router.RouteConfig[CreateUserReq, CreateUserResp]{
Path: "/users", // Path relative to the sub-router prefix (/api/v1/users)
Methods: []router.HttpMethod{router.MethodPost},
AuthLevel: router.Ptr(router.AuthRequired), // Example: Requires authentication
AuthLevel: new(router.AuthRequired), // Example: Requires authentication
Codec: codec.NewJSONCodec[CreateUserReq, CreateUserResp](), // Assume codec exists
Handler: CreateUserHandler, // Assume this generic handler exists
// Middlewares, Overrides can be set here too, overriding sub-router settings
Expand Down Expand Up @@ -163,7 +163,7 @@ err := router.RegisterGenericRouteOnSubRouter[CreateUserReq, CreateUserResp](
router.RouteConfig[CreateUserReq, CreateUserResp]{
Path: "/users", // Path relative to the sub-router prefix
Methods: []router.HttpMethod{router.MethodPost},
AuthLevel: router.Ptr(router.AuthRequired),
AuthLevel: new(router.AuthRequired),
Codec: codec.NewJSONCodec[CreateUserReq, CreateUserResp](),
Handler: CreateUserHandler,
},
Expand All @@ -190,7 +190,7 @@ r := router.NewRouter[string, string](routerConfig, authFunction, userIdFromUser
// Define a new sub-router
adminSubRouter := router.SubRouterConfig{
PathPrefix: "/admin",
AuthLevel: router.Ptr(router.AuthRequired), // All admin routes require auth by default
AuthLevel: new(router.AuthRequired), // All admin routes require auth by default
Routes: []router.RouteDefinition{
router.RouteConfigBase{
Path: "/users",
Expand Down
6 changes: 3 additions & 3 deletions examples/auth-levels/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,13 @@ func main() {
router.RouteConfigBase{
Path: "/no-auth",
Methods: []router.HttpMethod{router.MethodGet},
AuthLevel: router.Ptr(router.NoAuth), // Changed
AuthLevel: new(router.NoAuth), // Changed
Handler: noAuthHandler,
},
router.RouteConfigBase{ // Add explicit type
Path: "/optional-auth",
Methods: []router.HttpMethod{router.MethodGet},
AuthLevel: router.Ptr(router.AuthOptional), // Authentication is optional. OPTIONS requests are automatically allowed.
AuthLevel: new(router.AuthOptional), // Authentication is optional. OPTIONS requests are automatically allowed.
Middlewares: []common.Middleware{
middleware.AuthenticationWithUser[*User](customUserAuth), // Middleware to add user to context if authenticated
},
Expand All @@ -141,7 +141,7 @@ func main() {
router.RouteConfigBase{ // Add explicit type
Path: "/required-auth",
Methods: []router.HttpMethod{router.MethodGet},
AuthLevel: router.Ptr(router.AuthRequired), // Authentication is required. OPTIONS requests are automatically allowed.
AuthLevel: new(router.AuthRequired), // Authentication is required. OPTIONS requests are automatically allowed.
Middlewares: []common.Middleware{
middleware.AuthenticationWithUser[*User](customUserAuth), // Middleware to add user to context if authenticated
},
Expand Down
2 changes: 1 addition & 1 deletion examples/auth/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func main() {
router.RouteConfigBase{
Path: "/resource",
Methods: []router.HttpMethod{router.MethodGet},
AuthLevel: router.Ptr(router.AuthRequired), // Uses the router's internal authRequiredMiddleware. OPTIONS requests are automatically allowed.
AuthLevel: new(router.AuthRequired), // Uses the router's internal authRequiredMiddleware. OPTIONS requests are automatically allowed.
Handler: protectedHandler,
},
},
Expand Down
8 changes: 4 additions & 4 deletions examples/handler-error-middleware/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func main() {
router.RegisterGenericRoute(r, router.RouteConfig[CreateUserRequest, CreateUserResponse]{
Path: "/users/success",
Methods: []router.HttpMethod{router.MethodPost},
AuthLevel: router.Ptr(router.NoAuth),
AuthLevel: new(router.NoAuth),
Middlewares: []common.Middleware{
TransactionMiddleware,
ErrorLoggingMiddleware(logger),
Expand All @@ -135,7 +135,7 @@ func main() {
router.RegisterGenericRoute(r, router.RouteConfig[CreateUserRequest, CreateUserResponse]{
Path: "/users/validation-error",
Methods: []router.HttpMethod{router.MethodPost},
AuthLevel: router.Ptr(router.NoAuth),
AuthLevel: new(router.NoAuth),
Middlewares: []common.Middleware{
TransactionMiddleware,
ErrorLoggingMiddleware(logger),
Expand All @@ -154,7 +154,7 @@ func main() {
router.RegisterGenericRoute(r, router.RouteConfig[CreateUserRequest, CreateUserResponse]{
Path: "/users/internal-error",
Methods: []router.HttpMethod{router.MethodPost},
AuthLevel: router.Ptr(router.NoAuth),
AuthLevel: new(router.NoAuth),
Middlewares: []common.Middleware{
TransactionMiddleware,
ErrorLoggingMiddleware(logger),
Expand Down Expand Up @@ -194,7 +194,7 @@ func main() {
router.RegisterGenericRoute(r, router.RouteConfig[CreateUserRequest, CreateUserResponse]{
Path: "/users/custom-transaction",
Methods: []router.HttpMethod{router.MethodPost},
AuthLevel: router.Ptr(router.NoAuth),
AuthLevel: new(router.NoAuth),
Middlewares: []common.Middleware{
customTransactionMiddleware,
ErrorLoggingMiddleware(logger),
Expand Down
16 changes: 8 additions & 8 deletions examples/nested-subrouters/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ func main() {
router.RouteConfigBase{ // This type was already added, just confirming context
Path: "", // Becomes /api/v1/users
Methods: []router.HttpMethod{router.MethodGet},
AuthLevel: router.Ptr(router.NoAuth), // Changed
AuthLevel: new(router.NoAuth), // Changed
Handler: func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"users":[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"},{"id":3,"name":"Charlie"}]}`))
Expand All @@ -139,7 +139,7 @@ func main() {
router.RouteConfigBase{ // Add explicit type
Path: "/hello", // Becomes /api/v1/hello
Methods: []router.HttpMethod{router.MethodGet},
AuthLevel: router.Ptr(router.NoAuth), // Changed
AuthLevel: new(router.NoAuth), // Changed
Handler: func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"message":"Hello from API v1!"}`))
Expand Down Expand Up @@ -169,7 +169,7 @@ func main() {
router.RouteConfigBase{ // Add explicit type
Path: "/hello", // Becomes /api/v2/hello
Methods: []router.HttpMethod{router.MethodGet},
AuthLevel: router.Ptr(router.NoAuth), // Changed
AuthLevel: new(router.NoAuth), // Changed
Handler: func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"message":"Hello from API v2!"}`))
Expand All @@ -186,7 +186,7 @@ func main() {
router.RouteConfigBase{ // Add explicit type
Path: "/status", // Becomes /api/status
Methods: []router.HttpMethod{router.MethodGet},
AuthLevel: router.Ptr(router.NoAuth), // Changed
AuthLevel: new(router.NoAuth), // Changed
Handler: func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status":"ok"}`))
Expand Down Expand Up @@ -220,7 +220,7 @@ func main() {
router.RouteConfig[GreetingRequest, GreetingResponse]{
Path: "/greet", // Relative path
Methods: []router.HttpMethod{router.MethodPost},
AuthLevel: router.Ptr(router.NoAuth), // Changed
AuthLevel: new(router.NoAuth), // Changed
Codec: greetingCodec,
Handler: greetingHandler,
},
Expand All @@ -236,7 +236,7 @@ func main() {
router.RouteConfig[UserRequest, UserResponse]{
Path: "/info", // Relative path
Methods: []router.HttpMethod{router.MethodPost},
AuthLevel: router.Ptr(router.NoAuth), // Changed
AuthLevel: new(router.NoAuth), // Changed
Codec: userCodec,
Handler: userHandler,
},
Expand All @@ -252,7 +252,7 @@ func main() {
router.RouteConfig[UserRequest, UserResponse]{
Path: "/info", // Relative path
Methods: []router.HttpMethod{router.MethodPost},
AuthLevel: router.Ptr(router.NoAuth), // Changed
AuthLevel: new(router.NoAuth), // Changed
Codec: userCodec,
Handler: userHandler,
},
Expand All @@ -268,7 +268,7 @@ func main() {
router.RouteConfig[ProfileRequest, ProfileResponse]{
Path: "/profile", // Relative path
Methods: []router.HttpMethod{router.MethodPost},
AuthLevel: router.Ptr(router.AuthRequired), // Changed - This route requires authentication
AuthLevel: new(router.AuthRequired), // Changed - This route requires authentication
Codec: profileCodec,
Handler: profileHandler,
},
Expand Down
Loading
Loading