forked from gernest/utron
-
Notifications
You must be signed in to change notification settings - Fork 0
/
routes.go
486 lines (427 loc) · 12.6 KB
/
routes.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
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
package utron
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"reflect"
"strings"
"github.com/BurntSushi/toml"
"github.com/gernest/ita"
"github.com/gorilla/mux"
"github.com/hashicorp/hcl"
"github.com/justinas/alice"
"gopkg.in/yaml.v2"
)
var (
// ErrRouteStringFormat is returned when the route string is of the wrong format
ErrRouteStringFormat = errors.New("wrong route string, example is\" get,post;/hello/world;Hello\"")
)
// Router registers routes and handlers. It embeds gorilla mux Router
type Router struct {
*mux.Router
app *App
routes []*route
}
// NewRouter returns a new Router, if app is passed then it is used
func NewRouter(app ...*App) *Router {
var dApp *App
if len(app) > 0 {
dApp = app[0]
}
return &Router{
Router: mux.NewRouter(),
app: dApp,
}
}
// route tracks information about http route
type route struct {
pattern string // url pattern e.g /home
methods []string // http methods e.g GET, POST etc
ctrl string // the name of the controller
fn string // the name of the controller's method to be executed
}
// Add registers ctrl. It takes additional comma separated list of middleware. middlewares
// are of type
// func(http.Handler)http.Handler
// or
// func(*Context)error
//
// utron uses the alice package to chain middlewares, this means all alice compatible middleware
// works out of the box
func (r *Router) Add(ctrlfn func() Controller, middlewares ...interface{}) error {
var (
// routes is a slice of all routes associated
// with ctrl
routes = struct {
inCtrl, standard []*route
}{}
// baseController is the name of the Struct BaseController
// when users embed the BaseController, an anonymous field
// BaseController is added, and here we are referring to the name of the
// anonymous field
baseController = "BaseController"
// routePaths is the name of the field that allows uses to add Routes information
routePaths = "Routes"
)
baseCtr := reflect.ValueOf(&BaseController{})
ctrlVal := reflect.ValueOf(ctrlfn())
bTyp := baseCtr.Type()
cTyp := ctrlVal.Type()
numCtr := cTyp.NumMethod()
ctrlName := getTypName(cTyp) // The name of the controller
for v := range make([]struct{}, numCtr) {
method := cTyp.Method(v)
// skip methods defined by the base controller
if _, bok := bTyp.MethodByName(method.Name); bok {
continue
}
// patt composes pattern. This can be overridden by routes defined in the Routes
// field of the controller.
// By default the path is of the form /:controller/:method. All http methods will be registered
// for this pattern, meaning it is up to the user to filter out what he/she wants, the easier way
// is to use the Routes field instead
//
// TODD: figure out the way of passing parameters to the method arguments?
patt := "/" + strings.ToLower(ctrlName) + "/" + strings.ToLower(method.Name)
r := &route{
pattern: patt,
ctrl: ctrlName,
fn: method.Name,
}
routes.standard = append(routes.standard, r)
}
// ultimate returns the actual value stored in rVals this means if rVals is a pointer,
// then we return the value that is pointed to. We are dealing with structs, so the returned
// value is of kind reflect.Struct
ultimate := func(rVals reflect.Value) reflect.Value {
val := rVals
switch val.Kind() {
case reflect.Ptr:
val = val.Elem()
}
return val
}
uCtr := ultimate(ctrlVal) // actual value after dereferencing the pointer
uCtrTyp := uCtr.Type() // we store the type, so we can use in the next iterations
for k := range make([]struct{}, uCtr.NumField()) {
// We iterate in all fields, to filter out the user defined methods. We are aware
// of methods inherited from the BaseController. Since we recommend user Controllers
// should embed BaseController
field := uCtrTyp.Field(k)
// If we find any field matching BaseController
// we initialize its value.
if field.Name == baseController {
fieldVal := uCtr.Field(k)
fieldVal.Set(reflect.ValueOf(new(BaseController)))
continue
}
// If there is any field named Routes, and it is of signature []string
// then the field's value is used to override the patterns defined earlier.
//
// It is not necessary for every user implementation to define method named Routes
// If we can't find it then we just ignore its use and fall-back to defaults.
//
// Route strings, are of the form "httpMethods;path;method"
// where httMethod: is a comma separated http method strings
// e.g GET,POST,PUT.
// The case does not matter, you can use lower case or upper case characters
// or even mixed case, that is get,GET,gET and GeT will all be treated as GET
//
// path: Is a url path or pattern, utron uses gorilla mux package. So, everything you can do
// with gorilla mux url path then you can do here.
// e.g /hello/{world}
// Don't worry about the params, they will be accessible via .Ctx.Params field in your
// controller.
//
// method: The name of the user Controller method to execute for this route.
if field.Name == routePaths {
fieldVal := uCtr.Field(k)
switch fieldVal.Kind() {
case reflect.Slice:
if data, ok := fieldVal.Interface().([]string); ok {
for _, d := range data {
rt, err := splitRoutes(d)
if err != nil {
continue
}
routes.inCtrl = append(routes.inCtrl, rt)
}
}
}
}
}
for _, v := range routes.standard {
var found bool
// use routes from the configuration file first
for _, rFile := range r.routes {
if rFile.ctrl == v.ctrl && rFile.fn == v.fn {
if err := r.add(rFile, ctrlfn, middlewares...); err != nil {
return err
}
found = true
}
}
// if there is no match from the routes file, use the routes defined in the Routes field
if !found {
for _, rFile := range routes.inCtrl {
if rFile.fn == v.fn {
if err := r.add(rFile, ctrlfn, middlewares...); err != nil {
return err
}
found = true
}
}
}
// resolve to sandard when everything else never matched
if !found {
if err := r.add(v, ctrlfn, middlewares...); err != nil {
return err
}
}
}
return nil
}
// getTypName returns a string representing the name of the object typ.
// if the name is defined then it is used, otherwise, the name is derived from the
// Stringer interface.
//
// the stringer returns something like *somepkg.MyStruct, so skip
// the *somepkg and return MyStruct
func getTypName(typ reflect.Type) string {
if typ.Name() != "" {
return typ.Name()
}
split := strings.Split(typ.String(), ".")
return split[len(split)-1]
}
// splitRoutes harvest the route components from routeStr.
func splitRoutes(routeStr string) (*route, error) {
// supported contains supported http methods
supported := "GET POST PUT PATCH TRACE PATCH DELETE HEAD OPTIONS"
// separator is a character used to separate route components from the routes string
separator := ";"
activeRoute := &route{}
if routeStr != "" {
s := strings.Split(routeStr, separator)
if len(s) != 3 {
return nil, ErrRouteStringFormat
}
m := strings.Split(s[0], ",")
for _, v := range m {
up := strings.ToUpper(v)
if !strings.Contains(supported, up) {
return nil, ErrRouteStringFormat
}
activeRoute.methods = append(activeRoute.methods, up)
}
p := s[1]
if !strings.Contains(p, "/") {
return nil, ErrRouteStringFormat
}
activeRoute.pattern = p
fn := strings.Split(s[2], ".")
switch len(fn) {
case 1:
activeRoute.fn = fn[0]
case 2:
activeRoute.ctrl = fn[0]
activeRoute.fn = fn[1]
default:
return nil, ErrRouteStringFormat
}
return activeRoute, nil
}
return nil, ErrRouteStringFormat
}
type middlewareTyp int
const (
plainMiddleware middlewareTyp = iota
ctxMiddleware
)
type middleware struct {
typ middlewareTyp
value interface{}
}
func (m *middleware) ToHandler(ctx *Context) func(http.Handler) http.Handler {
if m.typ == plainMiddleware {
return m.value.(func(http.Handler) http.Handler)
}
fn := m.value.(func(*Context) error)
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := fn(ctx)
if err != nil {
return
}
h.ServeHTTP(w, r)
})
}
}
// add registers controller ctrl, using activeRoute. If middlewares are provided, utron uses
// alice package to chain middlewares.
func (r *Router) add(activeRoute *route, ctrlfn func() Controller, middlewares ...interface{}) error {
var m []*middleware
if len(middlewares) > 0 {
for _, v := range middlewares {
switch v.(type) {
case func(http.Handler) http.Handler:
m = append(m, &middleware{
typ: plainMiddleware,
value: v,
})
case func(*Context) error:
m = append(m, &middleware{
typ: ctxMiddleware,
value: v,
})
default:
return fmt.Errorf("unsupported middleware %v", v)
}
}
}
// register methods if any
if len(activeRoute.methods) > 0 {
r.HandleFunc(activeRoute.pattern, func(w http.ResponseWriter, req *http.Request) {
ctx := NewContext(w, req)
r.prepareContext(ctx)
chain := chainMiddleware(ctx, m...)
chain.ThenFunc(r.wrapController(ctx, activeRoute.fn, ctrlfn())).ServeHTTP(w, req)
}).Methods(activeRoute.methods...)
return nil
}
r.HandleFunc(activeRoute.pattern, func(w http.ResponseWriter, req *http.Request) {
ctx := NewContext(w, req)
r.prepareContext(ctx)
chain := chainMiddleware(ctx, m...)
chain.ThenFunc(r.wrapController(ctx, activeRoute.fn, ctrlfn())).ServeHTTP(w, req)
})
return nil
}
func chainMiddleware(ctx *Context, wares ...*middleware) alice.Chain {
if len(wares) > 0 {
var m []alice.Constructor
for _, v := range wares {
m = append(m, v.ToHandler(ctx))
}
return alice.New(m...)
}
return alice.New()
}
// prepareContext sets view,config and model on the ctx.
func (r *Router) prepareContext(ctx *Context) {
if r.app != nil {
if r.app.view != nil {
ctx.Set(r.app.view)
}
if r.app.cfg != nil {
ctx.Cfg = r.app.cfg
}
if r.app.model != nil {
ctx.DB = r.app.model
}
}
}
// executes the method fn on Controller ctrl, it sets context.
func (r *Router) handleController(ctx *Context, fn string, ctrl Controller) {
ctrl.New(ctx)
// execute the method
// TODO: better error handling?
if x := ita.New(ctrl).Call(fn); x.Error() != nil {
ctx.Set(http.StatusInternalServerError)
_, _ = ctx.Write([]byte(x.Error().Error()))
ctx.TextPlain()
_ = ctx.Commit()
return
}
err := ctx.Commit()
if err != nil {
logThis.Errors(err)
}
}
// wrapController wraps a controller ctrl with method fn, and returns http.HandleFunc
func (r *Router) wrapController(ctx *Context, fn string, ctrl Controller) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, req *http.Request) {
r.handleController(ctx, fn, ctrl)
}
}
type routeFile struct {
Routes []string `json:"routes" toml:"routes" yaml:"routes"`
}
// LoadRoutesFile loads routes from a json file. Example of the routes file.
// {
// "routes": [
// "get,post;/hello;Sample.Hello",
// "get,post;/about;Hello.About"
// ]
// }
//
// supported formats are json, toml, yaml and hcl with extension .json, .toml, .yml and .hcl respectively.
//
//TODO refactor the decoding part to a separate function? This part shares the same logic as the
// one found in NewConfig()
func (r *Router) LoadRoutesFile(file string) error {
rFile := &routeFile{}
data, err := ioutil.ReadFile(file)
if err != nil {
return err
}
switch filepath.Ext(file) {
case ".json":
err = json.Unmarshal(data, rFile)
if err != nil {
return err
}
case ".toml":
_, err = toml.Decode(string(data), rFile)
if err != nil {
return err
}
case ".yml":
err = yaml.Unmarshal(data, rFile)
if err != nil {
return err
}
case ".hcl":
obj, err := hcl.Parse(string(data))
if err != nil {
return err
}
if err = hcl.DecodeObject(&rFile, obj); err != nil {
return err
}
default:
return errors.New("utron: unsupported file format")
}
for _, v := range rFile.Routes {
parsedRoute, perr := splitRoutes(v)
if perr != nil {
logThis.Errors(fmt.Sprintf("utron: parsing route %s %v", v, perr))
continue
}
r.routes = append(r.routes, parsedRoute)
}
return nil
}
// loadRoutes searches for the route file i the cfgPath. The order of file lookup is
// as follows.
// * routes.json
// * routes.toml
// * routes.yml
// * routes.hcl
func (r *Router) loadRoutes(cfgPath string) {
exts := []string{".json", ".toml", ".yml", ".hcl"}
rFile := "routes"
for _, ext := range exts {
file := filepath.Join(cfgPath, rFile+ext)
_, err := os.Stat(file)
if os.IsNotExist(err) {
continue
}
_ = r.LoadRoutesFile(file)
break
}
}