-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
/
router.go
255 lines (214 loc) · 8.15 KB
/
router.go
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
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
package router
import (
"errors"
"net/http"
"sync"
"github.com/kataras/iris/v12/context"
"github.com/schollz/closestmatch"
)
// Router is the "director".
// Caller should provide a request handler (router implementation or root handler).
// Router is responsible to build the received request handler and run it
// to serve requests, based on the received context.Pool.
//
// User can refresh the router with `RefreshRouter` whenever a route's field is changed by him.
type Router struct {
mu sync.Mutex // for Downgrade, WrapRouter & BuildRouter,
// not indeed but we don't to risk its usage by third-parties.
requestHandler RequestHandler // build-accessible, can be changed to define a custom router or proxy, used on RefreshRouter too.
mainHandler http.HandlerFunc // init-accessible
wrapperFunc func(http.ResponseWriter, *http.Request, http.HandlerFunc)
cPool *context.Pool // used on RefreshRouter
routesProvider RoutesProvider
// key = subdomain
// value = closest of static routes, filled on `BuildRouter/RefreshRouter`.
closestPaths map[string]*closestmatch.ClosestMatch
}
// NewRouter returns a new empty Router.
func NewRouter() *Router {
return &Router{}
}
// RefreshRouter re-builds the router. Should be called when a route's state
// changed (i.e Method changed at serve-time).
func (router *Router) RefreshRouter() error {
return router.BuildRouter(router.cPool, router.requestHandler, router.routesProvider, true)
}
// ErrNotRouteAdder throws on `AddRouteUnsafe` when a registered `RequestHandler`
// does not implements the optional `AddRoute(*Route) error` method.
var ErrNotRouteAdder = errors.New("request handler does not implement AddRoute method")
// AddRouteUnsafe adds a route directly to the router's request handler.
// Works before or after Build state.
// Mainly used for internal cases like `iris.WithSitemap`.
// Do NOT use it on serve-time.
func (router *Router) AddRouteUnsafe(r *Route) error {
if h := router.requestHandler; h != nil {
if v, ok := h.(interface {
AddRoute(*Route) error
}); ok {
return v.AddRoute(r)
}
}
return ErrNotRouteAdder
}
// FindClosestPaths returns a list of "n" paths close to "path" under the given "subdomain".
//
// Order may change.
func (router *Router) FindClosestPaths(subdomain, searchPath string, n int) []string {
if router.closestPaths == nil {
return nil
}
cm, ok := router.closestPaths[subdomain]
if !ok {
return nil
}
list := cm.ClosestN(searchPath, n)
if len(list) == 1 && list[0] == "" {
// yes, it may return empty string as its first slice element when not found.
return nil
}
return list
}
// BuildRouter builds the router based on
// the context factory (explicit pool in this case),
// the request handler which manages how the main handler will multiplexes the routes
// provided by the third parameter, routerProvider (it's the api builder in this case) and
// its wrapper.
//
// Use of RefreshRouter to re-build the router if needed.
func (router *Router) BuildRouter(cPool *context.Pool, requestHandler RequestHandler, routesProvider RoutesProvider, force bool) error {
if requestHandler == nil {
return errors.New("router: request handler is nil")
}
if cPool == nil {
return errors.New("router: context pool is nil")
}
// build the handler using the routesProvider
if err := requestHandler.Build(routesProvider); err != nil {
return err
}
router.mu.Lock()
defer router.mu.Unlock()
// store these for RefreshRouter's needs.
if force {
router.cPool = cPool
router.requestHandler = requestHandler
router.routesProvider = routesProvider
} else {
if router.cPool == nil {
router.cPool = cPool
}
if router.requestHandler == nil {
router.requestHandler = requestHandler
}
if router.routesProvider == nil && routesProvider != nil {
router.routesProvider = routesProvider
}
}
// the important
router.mainHandler = func(w http.ResponseWriter, r *http.Request) {
ctx := cPool.Acquire(w, r)
// Note: we can't get all r.Context().Value key-value pairs
// and save them to ctx.values.
router.requestHandler.HandleRequest(ctx)
cPool.Release(ctx)
}
if router.wrapperFunc != nil { // if wrapper used then attach that as the router service
router.mainHandler = NewWrapper(router.wrapperFunc, router.mainHandler).ServeHTTP
}
// build closest.
subdomainPaths := make(map[string][]string)
for _, r := range router.routesProvider.GetRoutes() {
if !r.IsStatic() {
continue
}
subdomainPaths[r.Subdomain] = append(subdomainPaths[r.Subdomain], r.Path)
}
router.closestPaths = make(map[string]*closestmatch.ClosestMatch)
for subdomain, paths := range subdomainPaths {
router.closestPaths[subdomain] = closestmatch.New(paths, []int{3, 4, 6})
}
return nil
}
// Downgrade "downgrades", alters the router supervisor service(Router.mainHandler)
// algorithm to a custom one,
// be aware to change the global variables of 'ParamStart' and 'ParamWildcardStart'.
// can be used to implement a custom proxy or
// a custom router which should work with raw ResponseWriter, *Request
// instead of the Context(which again, can be retrieved by the Framework's context pool).
//
// Note: Downgrade will by-pass the Wrapper, the caller is responsible for everything.
// Downgrade is thread-safe.
func (router *Router) Downgrade(newMainHandler http.HandlerFunc) {
router.mu.Lock()
router.mainHandler = newMainHandler
router.mu.Unlock()
}
// Downgraded returns true if this router is downgraded.
func (router *Router) Downgraded() bool {
return router.mainHandler != nil && router.requestHandler == nil
}
// WrapperFunc is used as an expected input parameter signature
// for the WrapRouter. It's a "low-level" signature which is compatible
// with the net/http.
// It's being used to run or no run the router based on a custom logic.
type WrapperFunc func(w http.ResponseWriter, r *http.Request, firstNextIsTheRouter http.HandlerFunc)
// WrapRouter adds a wrapper on the top of the main router.
// Usually it's useful for third-party middleware
// when need to wrap the entire application with a middleware like CORS.
//
// Developers can add more than one wrappers,
// those wrappers' execution comes from last to first.
// That means that the second wrapper will wrap the first, and so on.
//
// Before build.
func (router *Router) WrapRouter(wrapperFunc WrapperFunc) {
if wrapperFunc == nil {
return
}
router.mu.Lock()
defer router.mu.Unlock()
if router.wrapperFunc != nil {
// wrap into one function, from bottom to top, end to begin.
nextWrapper := wrapperFunc
prevWrapper := router.wrapperFunc
wrapperFunc = func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
if next != nil {
nexthttpFunc := http.HandlerFunc(func(_w http.ResponseWriter, _r *http.Request) {
prevWrapper(_w, _r, next)
})
nextWrapper(w, r, nexthttpFunc)
}
}
}
router.wrapperFunc = wrapperFunc
}
// ServeHTTPC serves the raw context, useful if we have already a context, it by-pass the wrapper.
func (router *Router) ServeHTTPC(ctx context.Context) {
router.requestHandler.HandleRequest(ctx)
}
func (router *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
router.mainHandler(w, r)
}
// RouteExists reports whether a particular route exists
// It will search from the current subdomain of context's host, if not inside the root domain.
func (router *Router) RouteExists(ctx context.Context, method, path string) bool {
return router.requestHandler.RouteExists(ctx, method, path)
}
type wrapper struct {
router http.HandlerFunc // http.HandlerFunc to catch the CURRENT state of its .ServeHTTP on case of future change.
wrapperFunc func(http.ResponseWriter, *http.Request, http.HandlerFunc)
}
// NewWrapper returns a new http.Handler wrapped by the 'wrapperFunc'
// the "next" is the final "wrapped" input parameter.
//
// Application is responsible to make it to work on more than one wrappers
// via composition or func clojure.
func NewWrapper(wrapperFunc func(w http.ResponseWriter, r *http.Request, routerNext http.HandlerFunc), wrapped http.HandlerFunc) http.Handler {
return &wrapper{
wrapperFunc: wrapperFunc,
router: wrapped,
}
}
func (wr *wrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
wr.wrapperFunc(w, r, wr.router)
}