From 2786ca1960b444d6d00fe708036b76801dd0771e Mon Sep 17 00:00:00 2001 From: kataras Date: Sun, 13 Aug 2017 07:57:47 +0300 Subject: [PATCH] MVC Example - you can learn a lot | Asleep for your eyes (again) Former-commit-id: cd66c00b29c903b724763cb84cae96426934acb5 --- README.md | 1 + _examples/README.md | 4 +- .../tutorial/mvc/controllers/controller.go | 162 ++++++++++++++++++ _examples/tutorial/mvc/controllers/index.go | 12 ++ _examples/tutorial/mvc/controllers/user.go | 61 +++++++ _examples/tutorial/mvc/main.go | 24 +++ _examples/tutorial/mvc/models/user.go | 15 ++ .../tutorial/mvc/persistence/database.go | 10 ++ _examples/tutorial/mvc/views/index.html | 11 ++ _examples/tutorial/mvc/views/user/index.html | 17 ++ core/router/router_wildcard_root_test.go | 9 + 11 files changed, 325 insertions(+), 1 deletion(-) create mode 100644 _examples/tutorial/mvc/controllers/controller.go create mode 100644 _examples/tutorial/mvc/controllers/index.go create mode 100644 _examples/tutorial/mvc/controllers/user.go create mode 100644 _examples/tutorial/mvc/main.go create mode 100644 _examples/tutorial/mvc/models/user.go create mode 100644 _examples/tutorial/mvc/persistence/database.go create mode 100644 _examples/tutorial/mvc/views/index.html create mode 100644 _examples/tutorial/mvc/views/user/index.html diff --git a/README.md b/README.md index fc4dbc36..243c07dc 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ Iris is a fast, simple and efficient micro web framework for Go. It provides a b * [Tutorial: Online Visitors](_examples/tutorial/online-visitors) * [Tutorial: URL Shortener using BoltDB](https://medium.com/@kataras/a-url-shortener-service-using-go-iris-and-bolt-4182f0b00ae7) * [Tutorial: How to turn your Android Device into a fully featured Web Server (**MUST**)](https://twitter.com/ThePracticalDev/status/892022594031017988) + * [Tutorial: Controllers from scratch (**Coming soon as built'n feature, probably at v8.3**)](_examples/tutorial/mvc) * [POC: Convert the medium-sized project "Parrot" from native to Iris](https://github.com/iris-contrib/parrot) * [Middleware](middleware/) * [Dockerize](https://github.com/iris-contrib/cloud-native-go) diff --git a/_examples/README.md b/_examples/README.md index 8e20cb7a..f5a6add1 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -12,7 +12,8 @@ It doesn't always contain the "best ways" but it does cover each important featu - [Glimpse](overview/main.go) - [Tutorial: Online Visitors](tutorial/online-visitors/main.go) - [Tutorial: URL Shortener using BoltDB](https://medium.com/@kataras/a-url-shortener-service-using-go-iris-and-bolt-4182f0b00ae7) -- [Tutorial: How to turn your Android Device into a fully featured Web Server](https://medium.com/@kataras/how-to-turn-an-android-device-into-a-web-server-9816b28ab199) +- [Tutorial: How to turn your Android Device into a fully featured Web Server (**MUST**)](https://twitter.com/ThePracticalDev/status/892022594031017988) +- [Tutorial: Controllers from scratch (**Coming soon as built'n feature, probably at v8.3**)](tutorial/mvc) ### HTTP Listening @@ -193,6 +194,7 @@ iris cache library lives on its own [package](https://github.com/kataras/iris/tr > You're free to use your own favourite caching package if you'd like so. ### Sessions + iris session manager lives on its own [package](https://github.com/kataras/iris/tree/master/sessions). - [Overview](sessions/overview/main.go) diff --git a/_examples/tutorial/mvc/controllers/controller.go b/_examples/tutorial/mvc/controllers/controller.go new file mode 100644 index 00000000..4e9a7aa1 --- /dev/null +++ b/_examples/tutorial/mvc/controllers/controller.go @@ -0,0 +1,162 @@ +package controllers + +import ( + "reflect" + "strings" + // "unsafe" + + "github.com/kataras/iris" + "github.com/kataras/iris/context" + "github.com/kataras/iris/core/router" +) + +type Controller struct { + // path params. + Params *context.RequestParams + + // view properties. + Layout string + Tmpl string + Data map[string]interface{} + + // give access to the request context itself. + Ctx context.Context +} + +// all lowercase, so user can see only the fields +// that are necessary to him/her. +func (b *Controller) init(ctx context.Context) { + b.Ctx = ctx + b.Params = ctx.Params() + b.Data = make(map[string]interface{}, 0) +} + +func (b *Controller) exec() { + if v := b.Tmpl; v != "" { + if l := b.Layout; l != "" { + b.Ctx.ViewLayout(l) + } + if d := b.Data; d != nil { + for key, value := range d { + b.Ctx.ViewData(key, value) + } + } + b.Ctx.View(v) + } +} + +// get the field name at compile-time, +// will help us to catch any unexpected results on future versions. +var baseControllerName = reflect.TypeOf(Controller{}).Name() + +func RegisterController(app *iris.Application, path string, c interface{}) { + typ := reflect.TypeOf(c) + + if typ.Kind() != reflect.Ptr { + typ = reflect.PtrTo(typ) + } + + elem := typ.Elem() + + // check if "c" has the "Controller" typeof `Controller` field. + b, has := elem.FieldByName(baseControllerName) + if !has { + panic("controller should have a field of Controller type") + } + + baseControllerFieldIndex := b.Index[0] + persistenceFields := make(map[int]reflect.Value, 0) + + if numField := elem.NumField(); numField > 1 { + val := reflect.Indirect(reflect.ValueOf(c)) + + for i := 0; i < numField; i++ { + f := elem.Field(i) + valF := val.Field(i) + // catch persistence data by tags, i.e: + // MyData string `iris:"persistence"` + if t, ok := f.Tag.Lookup("iris"); ok { + if t == "persistence" { + persistenceFields[i] = reflect.ValueOf(val.Field(i).Interface()) + continue + } + } + + // catch persistence data by pointer, i.e: + // DB *Database + if f.Type.Kind() == reflect.Ptr { + if !valF.IsNil() { + persistenceFields[i] = reflect.ValueOf(val.Field(i).Interface()) + } + } + } + } + + // check if has Any() or All() + // if yes, then register all http methods and + // exit. + m, has := typ.MethodByName("Any") + if !has { + m, has = typ.MethodByName("All") + } + if has { + app.Any(path, + controllerToHandler(elem, persistenceFields, + baseControllerFieldIndex, m.Index)) + return + } + + // else search the entire controller + // for any compatible method function + // and register that. + for _, method := range router.AllMethods { + httpMethodFuncName := strings.Title(strings.ToLower(method)) + + m, has := typ.MethodByName(httpMethodFuncName) + if !has { + continue + } + + httpMethodIndex := m.Index + + app.Handle(method, path, + controllerToHandler(elem, persistenceFields, + baseControllerFieldIndex, httpMethodIndex)) + } + +} + +func controllerToHandler(elem reflect.Type, persistenceFields map[int]reflect.Value, + baseControllerFieldIndex, httpMethodIndex int) context.Handler { + return func(ctx context.Context) { + // create a new controller instance of that type(>ptr). + c := reflect.New(elem) + + // get the responsible method. + // Remember: + // To improve the performance + // we don't compare the ctx.Method()[HTTP Method] + // to the instance's Method, each handler is registered + // to a specific http method. + methodFunc := c.Method(httpMethodIndex) + + // get the Controller embedded field. + b, _ := c.Elem().Field(baseControllerFieldIndex).Addr().Interface().(*Controller) + + if len(persistenceFields) > 0 { + elem := c.Elem() + for index, value := range persistenceFields { + elem.Field(index).Set(value) + } + } + + // init the new controller instance. + b.init(ctx) + + // execute the responsible method for that handler. + methodFunc.Interface().(func())() + + // finally, execute the controller. + b.exec() + } +} diff --git a/_examples/tutorial/mvc/controllers/index.go b/_examples/tutorial/mvc/controllers/index.go new file mode 100644 index 00000000..3d9787f3 --- /dev/null +++ b/_examples/tutorial/mvc/controllers/index.go @@ -0,0 +1,12 @@ +package controllers + +// Index is our index example controller. +type Index struct { + Controller +} + +func (c *Index) Get() { + c.Tmpl = "index.html" + c.Data["title"] = "Index page" + c.Data["message"] = "Hello world!" +} diff --git a/_examples/tutorial/mvc/controllers/user.go b/_examples/tutorial/mvc/controllers/user.go new file mode 100644 index 00000000..4dfd359e --- /dev/null +++ b/_examples/tutorial/mvc/controllers/user.go @@ -0,0 +1,61 @@ +package controllers + +import ( + "time" + + "github.com/kataras/iris/_examples/tutorial/mvc/persistence" +) + +// User is our user example controller. +type User struct { + Controller + + // all fields with pointers(*) + // that are not nil + // and all fields with + // that are tagged with iris:"persistence"` + // are being persistence and kept + // between the requests, meaning that + // they will not be reset-ed on each new request, + // they will be the same for all requests. + CreatedAt time.Time `iris:"persistence"` + Title string `iris:"persistence"` + DB *persistence.Database +} + +func NewUserController(db *persistence.Database) *User { + return &User{ + CreatedAt: time.Now(), + Title: "User page", + DB: db, + } +} + +// Get serves using the User controller when HTTP Method is "GET". +func (c *User) Get() { + c.Tmpl = "user/index.html" + c.Data["title"] = c.Title + c.Data["username"] = "kataras " + c.Params.Get("userid") + c.Data["connstring"] = c.DB.Connstring + c.Data["uptime"] = time.Now().Sub(c.CreatedAt).Seconds() +} + +/* Can use more than one, the factory will make sure +that the correct http methods are being registed for this +controller, uncommend these if you want: + +func (c *User) Post() {} +func (c *User) Put() {} +func (c *User) Delete() {} +func (c *User) Connect() {} +func (c *User) Head() {} +func (c *User) Patch() {} +func (c *User) Options() {} +func (c *User) Trace() {} +*/ + +/* +func (c *User) All() {} +// OR +func (c *User) Any() {} +*/ diff --git a/_examples/tutorial/mvc/main.go b/_examples/tutorial/mvc/main.go new file mode 100644 index 00000000..105c4069 --- /dev/null +++ b/_examples/tutorial/mvc/main.go @@ -0,0 +1,24 @@ +package main + +import ( + "github.com/kataras/iris/_examples/tutorial/mvc/controllers" + "github.com/kataras/iris/_examples/tutorial/mvc/persistence" + + "github.com/kataras/iris" +) + +func main() { + app := iris.New() + app.RegisterView(iris.HTML("./views", ".html")) + + db := persistence.OpenDatabase("a fake db") + + controllers.RegisterController(app, "/", new(controllers.Index)) + + controllers.RegisterController(app, "/user/{userid:int}", + controllers.NewUserController(db)) + + // http://localhost/ + // http://localhost:8080/user/42 + app.Run(iris.Addr(":8080")) +} diff --git a/_examples/tutorial/mvc/models/user.go b/_examples/tutorial/mvc/models/user.go new file mode 100644 index 00000000..ab09ede0 --- /dev/null +++ b/_examples/tutorial/mvc/models/user.go @@ -0,0 +1,15 @@ +package models + +import ( + "time" +) + +// User is an example model. +type User struct { + ID int64 + Username string + Firstname string + Lastname string + CreatedAt time.Time + UpdatedAt time.Time +} diff --git a/_examples/tutorial/mvc/persistence/database.go b/_examples/tutorial/mvc/persistence/database.go new file mode 100644 index 00000000..8de23178 --- /dev/null +++ b/_examples/tutorial/mvc/persistence/database.go @@ -0,0 +1,10 @@ +package persistence + +// Database is our imaginary storage. +type Database struct { + Connstring string +} + +func OpenDatabase(connstring string) *Database { + return &Database{Connstring: connstring} +} diff --git a/_examples/tutorial/mvc/views/index.html b/_examples/tutorial/mvc/views/index.html new file mode 100644 index 00000000..34826bf1 --- /dev/null +++ b/_examples/tutorial/mvc/views/index.html @@ -0,0 +1,11 @@ + + + + {{.title}} + + + +

{{.message}}

+ + + \ No newline at end of file diff --git a/_examples/tutorial/mvc/views/user/index.html b/_examples/tutorial/mvc/views/user/index.html new file mode 100644 index 00000000..f5058ac5 --- /dev/null +++ b/_examples/tutorial/mvc/views/user/index.html @@ -0,0 +1,17 @@ + + + + {{.title}} + + + +

Hello {{.username}}

+ + + All fields inside a controller that are pointers or they tagged as `iris:"persistence"` are marked as persistence data, meaning + that they will not be reset-ed on each new request. +

Persistence data from *DB.Connstring: {{.connstring}}

+

Persistence data from CreatedAt `iris:"persistence"`: {{.uptime}} seconds

+ + + \ No newline at end of file diff --git a/core/router/router_wildcard_root_test.go b/core/router/router_wildcard_root_test.go index 2ca30652..da9dbd33 100644 --- a/core/router/router_wildcard_root_test.go +++ b/core/router/router_wildcard_root_test.go @@ -80,6 +80,15 @@ func TestRouterWildcardAndStatic(t *testing.T) { {"GET", "/some/static", h, []testRouteRequest{ {"GET", "", "/some/static", iris.StatusOK, same_as_request_path}, }}, + + {"GET", "/s/{p:path}", h2, []testRouteRequest{ + {"GET", "", "/s/that/is/wildcard", iris.StatusForbidden, same_as_request_path}, + {"GET", "", "/s/did", iris.StatusForbidden, same_as_request_path}, + {"GET", "", "/s1/that/is/wildcard", iris.StatusNotFound, from_status_code}, + }}, + {"GET", "/s/static", h, []testRouteRequest{ + {"GET", "", "/s/static", iris.StatusOK, same_as_request_path}, + }}, } testTheRoutes(t, tt, false)