Skip to content

Commit ca81e13

Browse files
authored
feat(router)!: add build and config identities (#123)
* feat(scontext): add runtime identity providers * feat(router)!: group application dependencies BREAKING CHANGE: NewRouter now accepts a RouterDependencies[T, U] value instead of separate authentication and user ID callbacks.
1 parent 17db3e2 commit ca81e13

66 files changed

Lines changed: 836 additions & 387 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import (
3636
func main() {
3737
r := router.NewRouter[string, string](router.RouterConfig{
3838
ServiceName: "hello-service",
39-
}, nil, nil)
39+
}, router.RouterDependencies[string, string]{})
4040

4141
r.Route(router.RouteConfigBase{
4242
Path: "/hello",
@@ -58,8 +58,8 @@ func main() {
5858
curl http://localhost:8080/hello
5959
```
6060

61-
The authentication callbacks may be nil when every route uses `NoAuth`, as in
62-
this example. A nil logger is replaced by a production logger, with a no-op
61+
Authentication dependencies may be omitted when every route uses `NoAuth`, as
62+
in this example. A nil logger is replaced by a production logger, with a no-op
6363
fallback if logger creation fails.
6464

6565
## Core model
@@ -137,6 +137,8 @@ not called. A failed build is terminal for that router; later mutation panics.
137137
built-in authentication stage to populate identity first.
138138
- `TraceIDBufferSize` controls trace-ID generation. `EnableTraceLogging`
139139
independently enables request-summary logs.
140+
- Optional build and config identity providers are sampled once per request and
141+
stored in the shared SRouter context.
140142
- Proxy headers are trusted only when explicitly configured. Review the IP
141143
guide before enabling them because client-IP choice affects security and
142144
rate-limit keys.

docs/authentication.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ A route-level value wins over its innermost group, and inner groups win over out
3131

3232
## Router authentication functions
3333

34-
Built-in authentication uses the callbacks passed to `NewRouter`:
34+
Built-in authentication uses `RouterDependencies` passed to `NewRouter`:
3535

3636
```go
3737
func authenticate(ctx context.Context, token string) (*User, bool) {
@@ -46,15 +46,18 @@ func userID(user *User) string {
4646
return user.ID
4747
}
4848

49-
r := router.NewRouter[string, User](config, authenticate, userID)
49+
r := router.NewRouter(config, router.RouterDependencies[string, User]{
50+
Authenticate: authenticate,
51+
UserID: userID,
52+
})
5053
```
5154

5255
On success, `authenticate` must return a usable `*User` and `true`. SRouter passes that pointer to `userID`, stores the resulting ID in `SRouterContext`, and stores the user pointer as well when `RouterConfig.AddUserObjectToCtx` is true.
5356

54-
The callbacks are required only if at least one compiled route resolves to `AuthOptional` or `AuthRequired`. `Build` fails with a descriptive error when such a route exists and either callback is nil. A router containing only `NoAuth` routes can use:
57+
The authentication dependencies are required only if at least one compiled route resolves to `AuthOptional` or `AuthRequired`. `Build` fails with a descriptive error when such a route exists and either dependency is nil. A router containing only `NoAuth` routes can use:
5558

5659
```go
57-
r := router.NewRouter[string, User](config, nil, nil)
60+
r := router.NewRouter[string, User](config, router.RouterDependencies[string, User]{})
5861
```
5962

6063
Calling `Build` during startup is recommended so callback and route configuration errors are reported before serving traffic. Otherwise the first request triggers the build.
@@ -148,7 +151,7 @@ Choose one of these arrangements when user-based limiting must use custom authen
148151
- Wrap the whole router with authentication before assigning it to `http.Server.Handler`:
149152

150153
```go
151-
r := router.NewRouter[string, User](config, nil, nil)
154+
r := router.NewRouter[string, User](config, router.RouterDependencies[string, User]{})
152155
srv := &http.Server{Handler: corsAwareAPIKeyAuth(r)}
153156
```
154157

docs/configuration.md

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,39 @@ type RouterConfig struct {
3737
Routes do not live inside `RouterConfig`. Add them after `NewRouter` with
3838
`Router.Route` and `Router.Group`.
3939

40-
The authentication callbacks passed to `NewRouter` may be nil when every route
41-
resolves to `NoAuth`. If any route resolves to `AuthOptional` or `AuthRequired`,
42-
`Build` requires both the token-validation callback and the user-ID extraction
43-
callback.
40+
## `RouterDependencies[T, U]`
41+
42+
Application behavior is grouped separately from static router configuration:
43+
44+
```go
45+
type RouterDependencies[T comparable, U any] struct {
46+
Authenticate func(context.Context, string) (*U, bool)
47+
UserID func(*U) T
48+
BuildID func() string
49+
ConfigID func() string
50+
}
51+
52+
r := router.NewRouter(config, router.RouterDependencies[string, User]{
53+
Authenticate: authenticate,
54+
UserID: userIDFromUser,
55+
BuildID: currentBuildID,
56+
ConfigID: currentConfigID,
57+
})
58+
```
59+
60+
`Authenticate` and `UserID` may be nil when every route resolves to `NoAuth`.
61+
If any route resolves to `AuthOptional` or `AuthRequired`, `Build` requires both.
62+
63+
`BuildID` and `ConfigID` are optional. SRouter invokes each non-nil resolver once
64+
per request and stores a non-empty result in the shared SRouter context. Returned
65+
strings are opaque, log-safe identifiers: SRouter does not parse, normalize,
66+
cache, or propagate them through headers. Resolvers must be concurrency-safe,
67+
fast, and non-panicking.
68+
69+
This replaces the older three-argument constructor. Migrate
70+
`NewRouter(config, authenticate, userIDFromUser)` by wrapping the callbacks in a
71+
single `RouterDependencies` value as shown above. For a `NoAuth` router, pass an
72+
empty typed value such as `RouterDependencies[string, User]{}`.
4473

4574
The router itself is the root group. Its fluent `Use`, `Timeout`,
4675
`MaxBodySize`, `RateLimit`, `AuthToken`, and `Auth` methods override global

docs/context-management.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ with the wrapper's internal lock.
2020
| Client IP | `WithClientIP`, `WithClientInfo` | `GetClientIP`, `GetClientIPFromRequest` |
2121
| User agent | `WithUserAgent`, `WithClientInfo` | `GetUserAgent`, `GetUserAgentFromRequest` |
2222
| Trace ID | `WithTraceID` | `GetTraceIDFromContext`, `GetTraceIDFromRequest` |
23+
| Build identity | `WithBuildID` | `GetBuildID`, `GetBuildIDFromRequest` |
24+
| Configuration identity | `WithConfigID` | `GetConfigID`, `GetConfigIDFromRequest` |
2325
| Database transaction | `WithTransaction` | `GetTransaction`, `GetTransactionFromRequest` |
2426
| Route template and path parameters | `WithRouteInfo`, `SetRouteInfo` | `GetRouteTemplateFromRequest`, `GetPathParamsFromRequest` |
2527
| Allowed CORS origin and credentials | `WithCORSInfo` | `GetCORSInfo`, `GetCORSInfoFromRequest` |
@@ -32,6 +34,17 @@ its zero value. The trace-ID getters instead return an empty string when no
3234
trace ID is set. `WithTraceID` preserves an existing ID rather than overwriting
3335
one propagated by an upstream service.
3436

37+
Applications may configure `RouterDependencies.BuildID` and
38+
`RouterDependencies.ConfigID` to install opaque, log-safe runtime identities.
39+
SRouter samples each non-nil provider once at the beginning of every request,
40+
before CORS, routing, and middleware. Empty results remain unset; a non-empty
41+
local result replaces an inherited identity. Providers must be concurrency-safe,
42+
fast, and non-panicking.
43+
44+
These identities are not propagated through request or response headers.
45+
Non-HTTP work, such as background workers, can install already-sampled values
46+
with `WithBuildID` and `WithConfigID`.
47+
3548
The router populates client information and, after a route match, its route
3649
template and path parameters. When CORS is configured, CORS information is
3750
stored even when the request has no `Origin` or the origin is denied; an empty

docs/getting-started.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ func main() {
3636
GlobalMaxBodySize: 1 << 20,
3737
}
3838

39-
r := router.NewRouter[string, string](config, nil, nil)
39+
r := router.NewRouter[string, string](config, router.RouterDependencies[string, string]{})
4040
r.Route(router.RouteConfigBase{
4141
Path: "/hello",
4242
Methods: []router.HttpMethod{router.MethodGet},
@@ -67,7 +67,7 @@ with `r.Route`; recursive path scopes are created with `r.Group`. Both root and
6767
group `Route` methods accept standard `RouteConfigBase` values and typed
6868
`RouteConfig[Req, Resp]` values.
6969

70-
The authentication callbacks may be nil while every effective route auth level
70+
Authentication dependencies may be nil while every effective route auth level
7171
is `NoAuth`. Supply them before adding `AuthOptional` or `AuthRequired` routes.
7272

7373
Calling `Build` during startup is recommended. It validates the full route tree

docs/logging.md

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@ if err != nil {
99
}
1010
defer logger.Sync()
1111

12-
r := router.NewRouter[string, User](router.RouterConfig{
12+
r := router.NewRouter(router.RouterConfig{
1313
Logger: logger,
14-
}, authenticate, userIDFromUser)
14+
}, router.RouterDependencies[string, User]{
15+
Authenticate: authenticate,
16+
UserID: userIDFromUser,
17+
BuildID: func() string { return buildID },
18+
ConfigID: func() string { return configID },
19+
})
1520
```
1621

1722
## Request summary logging
@@ -21,7 +26,17 @@ For requests that reach normal route dispatch, SRouter emits one `"Request summa
2126
- `TraceIDBufferSize > 0`, which enables automatic trace IDs on matched routes and request summaries.
2227
- `EnableTraceLogging`, which enables request summaries independently of trace IDs.
2328

24-
The summary contains `method`, `path`, `status`, `duration`, `bytes`, `ip`, and `user_agent`. For a matched route, it also contains `trace_id` when automatic trace generation is enabled. An unmatched 404 or 405 still receives a summary, but it never enters the per-route trace middleware and therefore has no automatically generated `trace_id`.
29+
The summary contains `method`, `path`, `status`, `duration`, `bytes`, `ip`, and `user_agent`. It also contains configured `build_id` and `config_id` values when available. For a matched route, it contains `trace_id` when automatic trace generation is enabled. An unmatched 404 or 405 still receives a summary, but it never enters the per-route trace middleware and therefore has no automatically generated `trace_id`.
30+
31+
SRouter adds available runtime identities to its request-bound authentication,
32+
timeout, panic recovery, handled HTTP error, lazy-build failure, and JSON-response
33+
write-failure logs. Startup and route-registration logs have no request context
34+
and remain unchanged.
35+
36+
Runtime identities are opaque, log-safe application values. SRouter samples
37+
them once per request and does not propagate them through headers. Background
38+
workers may install already-sampled values with `scontext.WithBuildID` and
39+
`scontext.WithConfigID`.
2540

2641
Its level is chosen in this priority order:
2742

@@ -116,10 +131,13 @@ idGenerator := middleware.NewIDGenerator(1000)
116131
defer idGenerator.Stop()
117132

118133
traceMiddleware := middleware.CreateTraceMiddleware[string, User](idGenerator)
119-
r := router.NewRouter[string, User](router.RouterConfig{
134+
r := router.NewRouter(router.RouterConfig{
120135
Logger: logger,
121136
EnableTraceLogging: true,
122-
}, authenticate, userIDFromUser)
137+
}, router.RouterDependencies[string, User]{
138+
Authenticate: authenticate,
139+
UserID: userIDFromUser,
140+
})
123141

124142
handler := traceMiddleware(r)
125143
```

docs/metrics.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ config := router.RouterConfig{
2626
},
2727
}
2828

29-
r := router.NewRouter[string, User](config, authenticate, userIDFromUser)
29+
r := router.NewRouter(config, router.RouterDependencies[string, User]{
30+
Authenticate: authenticate,
31+
UserID: userIDFromUser,
32+
})
3033
```
3134

3235
With the built-in middleware, non-empty `Namespace` and `Subsystem` values become the default tags `service` and `subsystem`, respectively. They do not configure a backend's native namespace or subsystem. For example, the Prometheus adapter's namespace and subsystem are separately supplied to `prometheus.NewPrometheusRegistry` and become metric-name prefixes.

docs/middleware.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,12 @@ values and concurrency rules.
4242
Middleware can be attached at four scopes:
4343

4444
```go
45-
r := router.NewRouter[string, User](router.RouterConfig{
45+
r := router.NewRouter(router.RouterConfig{
4646
Middlewares: []common.Middleware{globalAudit},
47-
}, authenticate, userIDFromUser)
47+
}, router.RouterDependencies[string, User]{
48+
Authenticate: authenticate,
49+
UserID: userIDFromUser,
50+
})
4851

4952
r.Use(rootHeaders)
5053

docs/rate-limiting.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ config := router.RouterConfig{
1212
},
1313
}
1414

15-
r := router.NewRouter[string, User](config, authenticate, userID)
15+
r := router.NewRouter(config, router.RouterDependencies[string, User]{
16+
Authenticate: authenticate,
17+
UserID: userID,
18+
})
1619

1720
r.Group("/account").
1821
Auth(router.AuthRequired).

docs/route-groups.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ SRouter has one runtime `Router` and one underlying `httprouter` dispatcher.
55
not independent HTTP handlers.
66

77
```go
8-
r := router.NewRouter[string, User](config, authenticate, userID)
8+
r := router.NewRouter(config, router.RouterDependencies[string, User]{
9+
Authenticate: authenticate,
10+
UserID: userID,
11+
})
912

1013
api := r.Group("/api").
1114
Timeout(3 * time.Second).

0 commit comments

Comments
 (0)