-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathoptions.go
More file actions
228 lines (204 loc) · 7.4 KB
/
Copy pathoptions.go
File metadata and controls
228 lines (204 loc) · 7.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
package shiftapi
import (
"net/http"
"reflect"
)
// sharedConfig is the common interface implemented by [*API], [*groupConfig],
// and [*routeConfig]. It provides the operations that are meaningful at all
// three levels: adding errors, middleware, and static response headers.
type sharedConfig interface {
addError(errorEntry)
addMiddleware([]func(http.Handler) http.Handler)
addStaticResponseHeader(staticResponseHeader)
}
// staticResponseHeader is a fixed name/value pair set on every response.
type staticResponseHeader struct {
name string
value string
}
// Option is the primary option type. It works at all levels: [New],
// [API.Group]/[Group.Group], and route registration functions ([Get], [Post],
// etc.). Options are composable via [ComposeOptions].
type Option func(sharedConfig)
func (f Option) applyToAPI(api *API) { f(api) }
func (f Option) applyToGroup(cfg *groupConfig) { f(cfg) }
func (f Option) applyToRoute(cfg *routeConfig) { f(cfg) }
// APIOption configures an [API] created with [New]. Both [Option] and
// API-specific options (like [WithInfo]) implement this interface.
type APIOption interface {
applyToAPI(*API)
}
// GroupOption configures a [Group] created with [API.Group] or [Group.Group].
// [Option] implements this interface.
type GroupOption interface {
applyToGroup(*groupConfig)
}
// RouteOption configures a route registered with [Get], [Post], [Put], etc.
// Both [Option] and route-specific options (like [WithStatus]) implement
// this interface.
type RouteOption interface {
applyToRoute(*routeConfig)
}
// apiOptionFunc is a function that implements [APIOption].
type apiOptionFunc func(*API)
func (f apiOptionFunc) applyToAPI(api *API) { f(api) }
// groupOptionFunc is a function that implements [GroupOption].
type groupOptionFunc func(*groupConfig)
func (f groupOptionFunc) applyToGroup(cfg *groupConfig) { f(cfg) }
// routeOptionFunc is a function that implements [RouteOption].
type routeOptionFunc func(*routeConfig)
func (f routeOptionFunc) applyToRoute(cfg *routeConfig) { f(cfg) }
// errorEntry maps an error type to an HTTP status code.
type errorEntry struct {
status int
typ reflect.Type // always pointer type for errors.As
}
// errorLookup maps concrete error types to their HTTP status codes.
// Built once at route registration time for O(1) lookups during error handling.
type errorLookup map[reflect.Type]int
func buildErrorLookup(entries []errorEntry) errorLookup {
if len(entries) == 0 {
return nil
}
lookup := make(errorLookup, len(entries)*2)
for _, e := range entries {
lookup[e.typ] = e.status // *T
lookup[e.typ.Elem()] = e.status // T (for value-receiver errors)
}
return lookup
}
// WithError declares that an error of type T may be returned at the given HTTP
// status code. T must implement [error] and its struct fields are reflected into
// the OpenAPI schema. At runtime, if a handler returns an error matching T (via
// [errors.As]), it is serialized as JSON with the declared status code.
//
// WithError returns an [Option] that works at any level:
//
// - [New] — applies to all routes (API-level)
//
// - [API.Group] / [Group.Group] — applies to all routes in the group
//
// - [Handle] — applies to a single route
//
// api := shiftapi.New(
// shiftapi.WithError[*AuthError](http.StatusUnauthorized),
// )
// v1 := api.Group("/api/v1",
// shiftapi.WithError[*RateLimitError](http.StatusTooManyRequests),
// )
// shiftapi.Handle(v1, "GET /users/{id}", getUser,
// shiftapi.WithError[*NotFoundError](http.StatusNotFound),
// )
func WithError[T error](status int) Option {
t := reflect.TypeFor[T]()
// Normalize to pointer so errors.As works correctly.
if t.Kind() != reflect.Pointer {
t = reflect.PointerTo(t)
}
return func(c sharedConfig) {
c.addError(errorEntry{status: status, typ: t})
}
}
// WithMiddleware applies standard HTTP middleware. Middleware functions are
// applied in order: the first argument wraps outermost.
//
// WithMiddleware returns an [Option] that works at any level:
//
// - [New] — applies to all routes (API-level)
//
// - [API.Group] / [Group.Group] — applies to all routes in the group
//
// - [Handle] — applies to a single route
//
// api := shiftapi.New(
// shiftapi.WithMiddleware(cors, logging),
// )
// v1 := api.Group("/api/v1",
// shiftapi.WithMiddleware(auth),
// )
// shiftapi.Handle(v1, "GET /admin", getAdmin,
// shiftapi.WithMiddleware(adminOnly),
// )
func WithMiddleware(mw ...func(http.Handler) http.Handler) Option {
return func(c sharedConfig) {
c.addMiddleware(mw)
}
}
// WithResponseHeader sets a static response header on every response. The
// header is also documented in the OpenAPI spec for each affected route.
//
// WithResponseHeader returns an [Option] that works at any level:
// - [New] — applies to all routes (API-level)
// - [API.Group] / [Group.Group] — applies to all routes in the group
// - [Handle] — applies to a single route
//
// Static headers are applied in API → Group → Route order. If the same header
// name is declared at multiple levels, the later level wins. Dynamic headers
// (header struct tags on the response type) are applied after static headers
// and take precedence for the same name.
//
// api := shiftapi.New(
// shiftapi.WithResponseHeader("X-Content-Type-Options", "nosniff"),
// )
// v1 := api.Group("/api/v1",
// shiftapi.WithResponseHeader("X-API-Version", "1"),
// )
// shiftapi.Handle(v1, "GET /users", listUsers,
// shiftapi.WithResponseHeader("Cache-Control", "max-age=3600"),
// )
func WithResponseHeader(name, value string) Option {
return func(c sharedConfig) {
c.addStaticResponseHeader(staticResponseHeader{name: http.CanonicalHeaderKey(name), value: value})
}
}
// ComposeOptions combines multiple [Option] values into a single [Option].
// Use this to create reusable option bundles that work at any level.
//
// func WithAuth() shiftapi.Option {
// return shiftapi.ComposeOptions(
// shiftapi.WithMiddleware(authMiddleware),
// shiftapi.WithError[*AuthError](http.StatusUnauthorized),
// )
// }
func ComposeOptions(opts ...Option) Option {
return func(c sharedConfig) {
for _, opt := range opts {
opt(c)
}
}
}
// ComposeAPIOptions combines multiple [APIOption] values into a single [APIOption].
// Since [Option] implements [APIOption], both shared and API-specific options
// can be mixed.
func ComposeAPIOptions(opts ...APIOption) APIOption {
return apiOptionFunc(func(api *API) {
for _, opt := range opts {
opt.applyToAPI(api)
}
})
}
// ComposeGroupOptions combines multiple [GroupOption] values into a single
// [GroupOption]. Since [Option] implements [GroupOption], both shared and
// group-specific options can be mixed.
func ComposeGroupOptions(opts ...GroupOption) GroupOption {
return groupOptionFunc(func(cfg *groupConfig) {
for _, opt := range opts {
opt.applyToGroup(cfg)
}
})
}
// ComposeRouteOptions combines multiple [RouteOption] values into a single
// [RouteOption]. Since [Option] implements [RouteOption], both shared and
// route-specific options can be mixed.
//
// createOpts := shiftapi.ComposeRouteOptions(
// shiftapi.WithStatus(http.StatusCreated),
// shiftapi.WithError[*ConflictError](http.StatusConflict),
// )
func ComposeRouteOptions(opts ...RouteOption) RouteOption {
return routeOptionFunc(func(cfg *routeConfig) {
for _, opt := range opts {
opt.applyToRoute(cfg)
}
})
}