From 591806795e596f96c2fb6dcc2b5ff77aea9d5e25 Mon Sep 17 00:00:00 2001 From: kataras Date: Sun, 27 Aug 2017 18:46:04 +0300 Subject: [PATCH] Update to 8.4.0 | New macro type, new high-optimized MVC features. Read HISTORY.md Former-commit-id: b72a23ba063be60a9750c8b1b0df024b0c8ed549 --- HISTORY.md | 42 +++ README.md | 2 +- VERSION | 2 +- _examples/README.md | 23 +- _examples/mvc/login/database/database.go | 20 ++ _examples/mvc/login/main.go | 41 +++ _examples/mvc/login/public/css/site.css | 61 ++++ _examples/mvc/login/user/auth.go | 125 ++++++++ _examples/mvc/login/user/controller.go | 125 ++++++++ _examples/mvc/login/user/datasource.go | 114 ++++++++ _examples/mvc/login/user/model.go | 36 +++ _examples/mvc/login/views/shared/error.html | 4 + _examples/mvc/login/views/shared/layout.html | 12 + _examples/mvc/login/views/user/login.html | 11 + _examples/mvc/login/views/user/me.html | 3 + _examples/mvc/login/views/user/notfound.html | 3 + _examples/mvc/login/views/user/register.html | 14 + core/router/api_builder.go | 19 +- core/router/macro.go | 1 + core/router/macro/interpreter/ast/ast.go | 5 + .../macro/interpreter/parser/parser_test.go | 8 + core/router/macro/macro.go | 8 +- doc.go | 23 +- iris.go | 2 +- mvc/activator/activator.go | 272 +++++------------- mvc/activator/binder.go | 12 +- mvc/activator/callable_control.go | 53 ---- mvc/activator/{ => field}/field.go | 59 ++-- mvc/activator/method_control.go | 81 ------ mvc/activator/methodfunc/func_caller.go | 61 ++++ mvc/activator/methodfunc/func_info.go | 92 ++++++ mvc/activator/methodfunc/func_path.go | 119 ++++++++ mvc/activator/methodfunc/methodfunc.go | 35 +++ .../{model_control.go => model/model.go} | 52 ++-- mvc/activator/persistence/persistence.go | 60 ++++ mvc/activator/persistence_data_control.go | 57 ---- mvc/controller_test.go | 38 +++ 37 files changed, 1242 insertions(+), 453 deletions(-) create mode 100644 _examples/mvc/login/database/database.go create mode 100644 _examples/mvc/login/main.go create mode 100644 _examples/mvc/login/public/css/site.css create mode 100644 _examples/mvc/login/user/auth.go create mode 100644 _examples/mvc/login/user/controller.go create mode 100644 _examples/mvc/login/user/datasource.go create mode 100644 _examples/mvc/login/user/model.go create mode 100644 _examples/mvc/login/views/shared/error.html create mode 100644 _examples/mvc/login/views/shared/layout.html create mode 100644 _examples/mvc/login/views/user/login.html create mode 100644 _examples/mvc/login/views/user/me.html create mode 100644 _examples/mvc/login/views/user/notfound.html create mode 100644 _examples/mvc/login/views/user/register.html delete mode 100644 mvc/activator/callable_control.go rename mvc/activator/{ => field}/field.go (77%) delete mode 100644 mvc/activator/method_control.go create mode 100644 mvc/activator/methodfunc/func_caller.go create mode 100644 mvc/activator/methodfunc/func_info.go create mode 100644 mvc/activator/methodfunc/func_path.go create mode 100644 mvc/activator/methodfunc/methodfunc.go rename mvc/activator/{model_control.go => model/model.go} (50%) create mode 100644 mvc/activator/persistence/persistence.go delete mode 100644 mvc/activator/persistence_data_control.go diff --git a/HISTORY.md b/HISTORY.md index c13e381e..b4994b37 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -18,6 +18,48 @@ Developers are not forced to upgrade if they don't really need it. Upgrade whene **How to upgrade**: Open your command-line and execute this command: `go get -u github.com/kataras/iris`. + +# Su, 27 August 2017 | v8.4.0 + +Add a new macro type for path parameters, `long`, it's the go type `int64`. + +```go +app.Get("/user/{id:long}", func(ctx context.Context) { + userID, _ := ctx.Params().GetInt64("id") +}) +``` + +And the promise we gave to you some days ago. + +The ability to pre-calculate, register and map different (relative) paths inside a single controller +with zero performance cost. + +Meaning that after a `go get -u github.com/kataras/iris` you will be able to use things like these: + +If `app.Controller("/user", new(user.Controller))` + +- `func(*Controller) Get()` - `GET:/user` , as usual. +- `func(*Controller) Post()` - `POST:/user`, as usual. +- `func(*Controller) GetLogin()` - `GET:/user/login` +- `func(*Controller) PostLogin()` - `POST:/user/login` +- `func(*Controller) GetProfileFollowers()` - `GET:/user/profile/followers` +- `func(*Controller) PostProfileFollowers()` - `POST:/user/profile/followers` +- `func(*Controller) GetBy(id int64)` - `GET:/user/{param:long}` +- `func(*Controller) PostBy(id int64)` - `POST:/user/{param:long}` + +If `app.Controller("/profile", new(profile.Controller))` + +- `func(*Controller) GetBy(username string)` - `GET:/profile/{param:string}` + +If `app.Controller("/assets", new(file.Controller))` + +- `func(*Controller) GetByWildard(path string)` - `GET:/assets/{param:path}` + + +**Example** can be found at: [_examples/mvc/login/user/controller.go](_examples/mvc/login/user/controller.go). + +## Pretty [awesome](https://github.com/kataras/iris/stargazers), right? + # We, 23 August 2017 | v8.3.4 Give read access to the current request context's route, a feature that many of you asked a lot. diff --git a/README.md b/README.md index dfd96c78..99f236d4 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Iris may have reached version 8, but we're not stopping there. We have many feat ### 📑 Table of contents * [Installation](#-installation) -* [Latest changes](https://github.com/kataras/iris/blob/master/HISTORY.md#we-23-august-2017--v834) +* [Latest changes](https://github.com/kataras/iris/blob/master/HISTORY.md#su-27-august-2017--v840) * [Learn](#-learn) * [HTTP Listening](_examples/#http-listening) * [Configuration](_examples/#configuration) diff --git a/VERSION b/VERSION index fee113d0..c5ec764b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.3.4:https://github.com/kataras/iris/blob/master/HISTORY.md#we-23-august-2017--v834 \ No newline at end of file +8.4.0:https://github.com/kataras/iris/blob/master/HISTORY.md#su-27-august-2017--v840 \ No newline at end of file diff --git a/_examples/README.md b/_examples/README.md index ea8042b6..2498295f 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -136,6 +136,27 @@ Optional `EndRequest(ctx)` function to perform any finalization after any method Inheritance, see for example our `mvc.SessionController`, it has the `mvc.Controller` as an embedded field and it adds its logic to its `BeginRequest`, [here](https://github.com/kataras/iris/blob/master/mvc/session_controller.go). +Register one or more relative paths and able to get path parameters, i.e + +If `app.Controller("/user", new(user.Controller))` + +- `func(*Controller) Get()` - `GET:/user` , as usual. +- `func(*Controller) Post()` - `POST:/user`, as usual. +- `func(*Controller) GetLogin()` - `GET:/user/login` +- `func(*Controller) PostLogin()` - `POST:/user/login` +- `func(*Controller) GetProfileFollowers()` - `GET:/user/profile/followers` +- `func(*Controller) PostProfileFollowers()` - `POST:/user/profile/followers` +- `func(*Controller) GetBy(id int64)` - `GET:/user/{param:long}` +- `func(*Controller) PostBy(id int64)` - `POST:/user/{param:long}` + +If `app.Controller("/profile", new(profile.Controller))` + +- `func(*Controller) GetBy(username string)` - `GET:/profile/{param:string}` + +If `app.Controller("/assets", new(file.Controller))` + +- `func(*Controller) GetByWildard(path string)` - `GET:/assets/{param:path}` + **Using Iris MVC for code reuse** By creating components that are independent of one another, developers are able to reuse components quickly and easily in other applications. The same (or similar) view for one application can be refactored for another application with different data because the view is simply handling how the data is being displayed to the user. @@ -148,7 +169,7 @@ Follow the examples below, - [Hello world](mvc/hello-world/main.go) - [Session Controller](mvc/session-controller/main.go) - [A simple but featured Controller with model and views](mvc/controller-with-model-and-view). - +- [Login showcase](mvc/login/main.go) **NEW** ### Subdomains diff --git a/_examples/mvc/login/database/database.go b/_examples/mvc/login/database/database.go new file mode 100644 index 00000000..5409c6ae --- /dev/null +++ b/_examples/mvc/login/database/database.go @@ -0,0 +1,20 @@ +package database + +// Result is our imaginary result, it will never be used, it's +// here to show you a method of doing these things. +type Result struct { + cur int +} + +// Next moves the cursor to the next result. +func (r *Result) Next() interface{} { + return nil +} + +// Database is our imaginary database interface, it will never be used here. +type Database interface { + Open(connstring string) error + Close() error + Query(q string) (result Result, err error) + Exec(q string) (lastInsertedID int64, err error) +} diff --git a/_examples/mvc/login/main.go b/_examples/mvc/login/main.go new file mode 100644 index 00000000..fc81a3f8 --- /dev/null +++ b/_examples/mvc/login/main.go @@ -0,0 +1,41 @@ +package main + +import ( + "time" + + "github.com/kataras/iris/_examples/mvc/login-example/user" + + "github.com/kataras/iris" + "github.com/kataras/iris/sessions" +) + +func main() { + app := iris.New() + app.Logger().SetLevel("debug") + + app.RegisterView(iris.HTML("./views", ".html").Layout("shared/layout.html")) + + app.StaticWeb("/public", "./public") + + manager := sessions.New(sessions.Config{ + Cookie: "sessioncookiename", + Expires: 24 * time.Hour, + }) + users := user.NewDataSource() + + app.Controller("/user", new(user.Controller), manager, users) + + // http://localhost:8080/user/register + // http://localhost:8080/user/login + // http://localhost:8080/user/me + // http://localhost:8080/user/logout + // http://localhost:8080/user/1 + app.Run(iris.Addr(":8080"), configure) +} + +func configure(app *iris.Application) { + app.Configure( + iris.WithoutServerError(iris.ErrServerClosed), + iris.WithCharset("UTF-8"), + ) +} diff --git a/_examples/mvc/login/public/css/site.css b/_examples/mvc/login/public/css/site.css new file mode 100644 index 00000000..163af622 --- /dev/null +++ b/_examples/mvc/login/public/css/site.css @@ -0,0 +1,61 @@ +/* Bordered form */ +form { + border: 3px solid #f1f1f1; +} + +/* Full-width inputs */ +input[type=text], input[type=password] { + width: 100%; + padding: 12px 20px; + margin: 8px 0; + display: inline-block; + border: 1px solid #ccc; + box-sizing: border-box; +} + +/* Set a style for all buttons */ +button { + background-color: #4CAF50; + color: white; + padding: 14px 20px; + margin: 8px 0; + border: none; + cursor: pointer; + width: 100%; +} + +/* Add a hover effect for buttons */ +button:hover { + opacity: 0.8; +} + +/* Extra style for the cancel button (red) */ +.cancelbtn { + width: auto; + padding: 10px 18px; + background-color: #f44336; +} + +/* Center the container */ + +/* Add padding to containers */ +.container { + padding: 16px; +} + +/* The "Forgot password" text */ +span.psw { + float: right; + padding-top: 16px; +} + +/* Change styles for span and cancel button on extra small screens */ +@media screen and (max-width: 300px) { + span.psw { + display: block; + float: none; + } + .cancelbtn { + width: 100%; + } +} \ No newline at end of file diff --git a/_examples/mvc/login/user/auth.go b/_examples/mvc/login/user/auth.go new file mode 100644 index 00000000..c4d9a5f6 --- /dev/null +++ b/_examples/mvc/login/user/auth.go @@ -0,0 +1,125 @@ +package user + +import ( + "errors" + "strconv" + "strings" + + "github.com/kataras/iris/context" + "github.com/kataras/iris/mvc" +) + +// paths +const ( + PathLogin = "/user/login" + PathLogout = "/user/logout" +) + +// the session key for the user id comes from the Session. +const ( + sessionIDKey = "UserID" +) + +// AuthController is the user authentication controller, a custom shared controller. +type AuthController struct { + mvc.SessionController + + Source *DataSource + User Model `iris:"model"` +} + +// BeginRequest saves login state to the context, the user id. +func (c *AuthController) BeginRequest(ctx context.Context) { + c.SessionController.BeginRequest(ctx) + + if userID := c.Session.Get(sessionIDKey); userID != nil { + ctx.Values().Set(sessionIDKey, userID) + } +} + +func (c *AuthController) fireError(err error) { + if err != nil { + c.Ctx.Application().Logger().Debug(err.Error()) + + c.Status = 400 + c.Data["Title"] = "User Error" + c.Data["Message"] = strings.ToUpper(err.Error()) + c.Tmpl = "shared/error.html" + } +} + +func (c *AuthController) redirectTo(id int64) { + if id > 0 { + c.Path = "/user/" + strconv.Itoa(int(id)) + } +} + +func (c *AuthController) createOrUpdate(firstname, username, password string) (user Model, err error) { + username = strings.Trim(username, " ") + if username == "" || password == "" || firstname == "" { + return user, errors.New("empty firstname, username or/and password") + } + + userToInsert := Model{ + Firstname: firstname, + Username: username, + password: password, + } // password is hashed by the Source. + + newUser, err := c.Source.InsertOrUpdate(userToInsert) + if err != nil { + return user, err + } + + return newUser, nil +} + +func (c *AuthController) isLoggedIn() bool { + // we don't search by session, we have the user id + // already by the `SaveState` middleware. + return c.Values.Get(sessionIDKey) != nil +} + +func (c *AuthController) verify(username, password string) (user Model, err error) { + if username == "" || password == "" { + return user, errors.New("please fill both username and password fields") + } + + u, found := c.Source.GetByUsername(username) + if !found { + // if user found with that username not found at all. + return user, errors.New("user with that username does not exist") + } + + if ok, err := ValidatePassword(password, u.HashedPassword); err != nil || !ok { + // if user found but an error occurred or the password is not valid. + return user, errors.New("please try to login with valid credentials") + } + + return u, nil +} + +// if logged in then destroy the session +// and redirect to the login page +// otherwise redirect to the registration page. +func (c *AuthController) logout() { + if c.isLoggedIn() { + // c.Manager is the Sessions manager created + // by the embedded SessionController, automatically. + c.Manager.DestroyByID(c.Session.ID()) + return + } + + c.Path = PathLogin +} + +// AllowUser will check if this client is a logged user, +// if not then it will redirect that guest to the login page +// otherwise it will allow the execution of the next handler. +func AllowUser(ctx context.Context) { + if ctx.Values().Get(sessionIDKey) != nil { + ctx.Next() + return + } + ctx.Redirect(PathLogin) +} diff --git a/_examples/mvc/login/user/controller.go b/_examples/mvc/login/user/controller.go new file mode 100644 index 00000000..8130ec00 --- /dev/null +++ b/_examples/mvc/login/user/controller.go @@ -0,0 +1,125 @@ +package user + +const ( + pathMyProfile = "/user/me" + pathRegister = "/user/register" +) + +// Controller is responsible to handle the following requests: +// GET /user/register +// POST /user/register +// GET /user/login +// POST /user/login +// GET /user/me +// GET /user/{id:long} | long is a new param type, it's the int64. +// All HTTP Methods /user/logout +type Controller struct { + AuthController +} + +// GetRegister handles GET:/user/register. +func (c *Controller) GetRegister() { + if c.isLoggedIn() { + c.logout() + return + } + + c.Data["Title"] = "User Registration" + c.Tmpl = pathRegister + ".html" +} + +// PostRegister handles POST:/user/register. +func (c *Controller) PostRegister() { + // we can either use the `c.Ctx.ReadForm` or read values one by one. + var ( + firstname = c.Ctx.FormValue("firstname") + username = c.Ctx.FormValue("username") + password = c.Ctx.FormValue("password") + ) + + user, err := c.createOrUpdate(firstname, username, password) + if err != nil { + c.fireError(err) + return + } + + // setting a session value was never easier. + c.Session.Set(sessionIDKey, user.ID) + // succeed, nothing more to do here, just redirect to the /user/me. + c.Path = pathMyProfile +} + +// GetLogin handles GET:/user/login. +func (c *Controller) GetLogin() { + if c.isLoggedIn() { + c.logout() + return + } + c.Data["Title"] = "User Login" + c.Tmpl = PathLogin + ".html" +} + +// PostLogin handles POST:/user/login. +func (c *Controller) PostLogin() { + var ( + username = c.Ctx.FormValue("username") + password = c.Ctx.FormValue("password") + ) + + user, err := c.verify(username, password) + if err != nil { + c.fireError(err) + return + } + + c.Session.Set(sessionIDKey, user.ID) + c.Path = pathMyProfile +} + +// AnyLogout handles any method on path /user/logout. +func (c *Controller) AnyLogout() { + c.logout() +} + +// GetMe handles GET:/user/me. +func (c *Controller) GetMe() { + id, err := c.Session.GetInt64(sessionIDKey) + if err != nil || id <= 0 { + // when not already logged in. + c.Path = PathLogin + return + } + + u, found := c.Source.GetByID(id) + if !found { + // if the session exists but for some reason the user doesn't exist in the "database" + // then logout him and redirect to the register page. + c.logout() + return + } + + // set the model and render the view template. + c.User = u + c.Data["Title"] = "Profile of " + u.Username + c.Tmpl = pathMyProfile + ".html" +} + +func (c *Controller) renderNotFound(id int64) { + c.Status = 404 + c.Data["Title"] = "User Not Found" + c.Data["ID"] = id + c.Tmpl = "user/notfound.html" +} + +// GetBy handles GET:/user/{id:long}, +// i.e http://localhost:8080/user/1 +func (c *Controller) GetBy(userID int64) { + // we have /user/{id} + // fetch and render user json. + if user, found := c.Source.GetByID(userID); !found { + // not user found with that ID. + c.renderNotFound(userID) + } else { + c.Ctx.JSON(user) + } +} diff --git a/_examples/mvc/login/user/datasource.go b/_examples/mvc/login/user/datasource.go new file mode 100644 index 00000000..2ce11773 --- /dev/null +++ b/_examples/mvc/login/user/datasource.go @@ -0,0 +1,114 @@ +package user + +import ( + "errors" + "sync" + "time" +) + +// IDGenerator would be our user ID generator +// but here we keep the order of users by their IDs +// so we will use numbers that can be easly written +// to the browser to get results back from the REST API. +// var IDGenerator = func() string { +// return uuid.NewV4().String() +// } + +// DataSource is our data store example. +type DataSource struct { + Users map[int64]Model + mu sync.RWMutex +} + +// NewDataSource returns a new user data source. +func NewDataSource() *DataSource { + return &DataSource{ + Users: make(map[int64]Model), + } +} + +// GetBy returns receives a query function +// which is fired for every single user model inside +// our imaginary database. +// When that function returns true then it stops the iteration. +// +// It returns the query's return last known boolean value +// and the last known user model +// to help callers to reduce the loc. +// +// But be carefully, the caller should always check for the "found" +// because it may be false but the user model has actually real data inside it. +// +// It's actually a simple but very clever prototype function +// I'm think of and using everywhere since then, +// hope you find it very useful too. +func (d *DataSource) GetBy(query func(Model) bool) (user Model, found bool) { + d.mu.RLock() + for _, user = range d.Users { + found = query(user) + if found { + break + } + } + d.mu.RUnlock() + return +} + +// GetByID returns a user model based on its ID. +func (d *DataSource) GetByID(id int64) (Model, bool) { + return d.GetBy(func(u Model) bool { + return u.ID == id + }) +} + +// GetByUsername returns a user model based on the Username. +func (d *DataSource) GetByUsername(username string) (Model, bool) { + return d.GetBy(func(u Model) bool { + return u.Username == username + }) +} + +func (d *DataSource) getLastID() (lastID int64) { + d.mu.RLock() + for id := range d.Users { + if id > lastID { + lastID = id + } + } + d.mu.RUnlock() + + return lastID +} + +// InsertOrUpdate adds or updates a user to the (memory) storage. +func (d *DataSource) InsertOrUpdate(user Model) (Model, error) { + // no matter what we will update the password hash + // for both update and insert actions. + hashedPassword, err := GeneratePassword(user.password) + if err != nil { + return user, err + } + user.HashedPassword = hashedPassword + + // update + if id := user.ID; id > 0 { + _, found := d.GetByID(id) + if !found { + return user, errors.New("ID should be zero or a valid one that maps to an existing User") + } + d.mu.Lock() + d.Users[id] = user + d.mu.Unlock() + return user, nil + } + + // insert + id := d.getLastID() + 1 + user.ID = id + d.mu.Lock() + user.CreatedAt = time.Now() + d.Users[id] = user + d.mu.Unlock() + + return user, nil +} diff --git a/_examples/mvc/login/user/model.go b/_examples/mvc/login/user/model.go new file mode 100644 index 00000000..c49c462a --- /dev/null +++ b/_examples/mvc/login/user/model.go @@ -0,0 +1,36 @@ +package user + +import ( + "time" + + "golang.org/x/crypto/bcrypt" +) + +// Model is our User example model. +type Model struct { + ID int64 `json:"id"` + Firstname string `json:"firstname"` + Username string `json:"username"` + // password is the client-given password + // which will not be stored anywhere in the server. + // It's here only for actions like registration and update password, + // because we caccept a Model instance + // inside the `DataSource#InsertOrUpdate` function. + password string + HashedPassword []byte `json:"-"` + CreatedAt time.Time `json:"created_at"` +} + +// GeneratePassword will generate a hashed password for us based on the +// user's input. +func GeneratePassword(userPassword string) ([]byte, error) { + return bcrypt.GenerateFromPassword([]byte(userPassword), bcrypt.DefaultCost) +} + +// ValidatePassword will check if passwords are matched. +func ValidatePassword(userPassword string, hashed []byte) (bool, error) { + if err := bcrypt.CompareHashAndPassword(hashed, []byte(userPassword)); err != nil { + return false, err + } + return true, nil +} diff --git a/_examples/mvc/login/views/shared/error.html b/_examples/mvc/login/views/shared/error.html new file mode 100644 index 00000000..aca44bcc --- /dev/null +++ b/_examples/mvc/login/views/shared/error.html @@ -0,0 +1,4 @@ +

Error.

+

An error occurred while processing your request.

+ +

{{.Message}}

\ No newline at end of file diff --git a/_examples/mvc/login/views/shared/layout.html b/_examples/mvc/login/views/shared/layout.html new file mode 100644 index 00000000..1e02677b --- /dev/null +++ b/_examples/mvc/login/views/shared/layout.html @@ -0,0 +1,12 @@ + + + + {{.Title}} + + + + + {{ yield }} + + + \ No newline at end of file diff --git a/_examples/mvc/login/views/user/login.html b/_examples/mvc/login/views/user/login.html new file mode 100644 index 00000000..132c739e --- /dev/null +++ b/_examples/mvc/login/views/user/login.html @@ -0,0 +1,11 @@ +
+
+ + + + + + + +
+
\ No newline at end of file diff --git a/_examples/mvc/login/views/user/me.html b/_examples/mvc/login/views/user/me.html new file mode 100644 index 00000000..17e2f0b3 --- /dev/null +++ b/_examples/mvc/login/views/user/me.html @@ -0,0 +1,3 @@ +

+ Welcome back {{.User.Firstname}}! +

\ No newline at end of file diff --git a/_examples/mvc/login/views/user/notfound.html b/_examples/mvc/login/views/user/notfound.html new file mode 100644 index 00000000..c0000bda --- /dev/null +++ b/_examples/mvc/login/views/user/notfound.html @@ -0,0 +1,3 @@ +

+ User with ID {{.ID}} does not exist. +

\ No newline at end of file diff --git a/_examples/mvc/login/views/user/register.html b/_examples/mvc/login/views/user/register.html new file mode 100644 index 00000000..79684d99 --- /dev/null +++ b/_examples/mvc/login/views/user/register.html @@ -0,0 +1,14 @@ +
+
+ + + + + + + + + + +
+
\ No newline at end of file diff --git a/core/router/api_builder.go b/core/router/api_builder.go index e53e286e..fc7dff13 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -197,6 +197,10 @@ func (api *APIBuilder) HandleMany(method string, relativePath string, handlers . paths := strings.Split(trimmedPath, " ") for _, p := range paths { if p != "" { + if method == "ANY" || method == "ALL" { + routes = append(routes, api.Any(p, handlers...)...) + continue + } routes = append(routes, api.Handle(method, p, handlers...)) } } @@ -511,12 +515,11 @@ func (api *APIBuilder) Any(relativePath string, handlers ...context.Handler) (ro // Read more at `/mvc#Controller`. func (api *APIBuilder) Controller(relativePath string, controller activator.BaseController, bindValues ...interface{}) (routes []*Route) { - registerFunc := func(method string, handlers ...context.Handler) { - if method == "ANY" || method == "ALL" { - routes = api.Any(relativePath, handlers...) - } else { - routes = append(routes, api.HandleMany(method, relativePath, handlers...)...) - } + + registerFunc := func(ifRelPath string, method string, handlers ...context.Handler) { + relPath := relativePath + ifRelPath + r := api.HandleMany(method, relPath, handlers...) + routes = append(routes, r...) } // bind any values to the controller's relative fields @@ -527,9 +530,7 @@ func (api *APIBuilder) Controller(relativePath string, controller activator.Base // and tag them with `iris:"persistence"`. // // don't worry it will never be handled if empty values. - err := activator.Register(controller, bindValues, nil, registerFunc) - - if err != nil { + if err := activator.Register(controller, bindValues, registerFunc); err != nil { api.reporter.Add("%v for path: '%s'", err, relativePath) } diff --git a/core/router/macro.go b/core/router/macro.go index 8deedd17..ba6969ba 100644 --- a/core/router/macro.go +++ b/core/router/macro.go @@ -36,6 +36,7 @@ func registerBuiltinsMacroFuncs(out *macro.Map) { // these can be overridden by the user, later on. registerStringMacroFuncs(out.String) registerIntMacroFuncs(out.Int) + registerIntMacroFuncs(out.Long) registerAlphabeticalMacroFuncs(out.Alphabetical) registerFileMacroFuncs(out.File) registerPathMacroFuncs(out.Path) diff --git a/core/router/macro/interpreter/ast/ast.go b/core/router/macro/interpreter/ast/ast.go index 299946a8..b67af162 100644 --- a/core/router/macro/interpreter/ast/ast.go +++ b/core/router/macro/interpreter/ast/ast.go @@ -21,6 +21,10 @@ const ( // Allows only numbers (0-9) // Declaration: /mypath/{myparam:int} ParamTypeInt + // ParamTypeLong is the integer, a number type. + // Allows only numbers (0-9) + // Declaration: /mypath/{myparam:long} + ParamTypeLong // ParamTypeAlphabetical is the alphabetical/letter type type. // Allows letters only (upper or lowercase) // Declaration: /mypath/{myparam:alphabetical} @@ -44,6 +48,7 @@ const ( var paramTypes = map[string]ParamType{ "string": ParamTypeString, "int": ParamTypeInt, + "long": ParamTypeLong, "alphabetical": ParamTypeAlphabetical, "file": ParamTypeFile, "path": ParamTypePath, diff --git a/core/router/macro/interpreter/parser/parser_test.go b/core/router/macro/interpreter/parser/parser_test.go index caec39dc..70c8f110 100644 --- a/core/router/macro/interpreter/parser/parser_test.go +++ b/core/router/macro/interpreter/parser/parser_test.go @@ -60,6 +60,7 @@ func TestParseParam(t *testing.T) { }, ErrorCode: 404, }}, // 0 + {true, ast.ParamStatement{ Src: "{id:int range(1,5)}", @@ -123,6 +124,13 @@ func TestParseParam(t *testing.T) { }, ErrorCode: 404, }}, // 7 + {true, + ast.ParamStatement{ + Src: "{id:long else 404}", + Name: "id", + Type: ast.ParamTypeLong, + ErrorCode: 404, + }}, // 8 } diff --git a/core/router/macro/macro.go b/core/router/macro/macro.go index 719d759f..865e35eb 100644 --- a/core/router/macro/macro.go +++ b/core/router/macro/macro.go @@ -208,7 +208,7 @@ func (m *Macro) getFunc(funcName string) ParamEvaluatorBuilder { // Map contains the default macros mapped to their types. // This is the manager which is used by the caller to register custom -// parameter functions per param-type (String, Int, Alphabetical, File, Path). +// parameter functions per param-type (String, Int, Long, Alphabetical, File, Path). type Map struct { // string type // anything @@ -216,6 +216,9 @@ type Map struct { // int type // only numbers (0-9) Int *Macro + // long an int64 type + // only numbers (0-9) + Long *Macro // alphabetical/letter type // letters only (upper or lowercase) Alphabetical *Macro @@ -241,6 +244,7 @@ func NewMap() *Map { // it allows everything, so no need for a regexp here. String: newMacro(func(string) bool { return true }), Int: newMacro(MustNewEvaluatorFromRegexp("^[0-9]+$")), + Long: newMacro(MustNewEvaluatorFromRegexp("^[0-9]+$")), Alphabetical: newMacro(MustNewEvaluatorFromRegexp("^[a-zA-Z ]+$")), File: newMacro(MustNewEvaluatorFromRegexp("^[a-zA-Z0-9_.-]*$")), // it allows everything, we have String and Path as different @@ -259,6 +263,8 @@ func (m *Map) Lookup(typ ast.ParamType) *Macro { switch typ { case ast.ParamTypeInt: return m.Int + case ast.ParamTypeLong: + return m.Long case ast.ParamTypeAlphabetical: return m.Alphabetical case ast.ParamTypeFile: diff --git a/doc.go b/doc.go index 14f9ad2a..8b2de6f8 100644 --- a/doc.go +++ b/doc.go @@ -35,7 +35,7 @@ Source code and other details for the project are available at GitHub: Current Version -8.3.4 +8.4.0 Installation @@ -825,6 +825,27 @@ and it adds its logic to its `BeginRequest`. Source file: https://github.com/kat Read access to the current route via the `Route` field. +Register one or more relative paths and able to get path parameters, i.e + + If `app.Controller("/user", new(user.Controller))` + + - `func(*Controller) Get()` - `GET:/user` , as usual. + - `func(*Controller) Post()` - `POST:/user`, as usual. + - `func(*Controller) GetLogin()` - `GET:/user/login` + - `func(*Controller) PostLogin()` - `POST:/user/login` + - `func(*Controller) GetProfileFollowers()` - `GET:/user/profile/followers` + - `func(*Controller) PostProfileFollowers()` - `POST:/user/profile/followers` + - `func(*Controller) GetBy(id int64)` - `GET:/user/{param:long}` + - `func(*Controller) PostBy(id int64)` - `POST:/user/{param:long}` + + If `app.Controller("/profile", new(profile.Controller))` + + - `func(*Controller) GetBy(username string)` - `GET:/profile/{param:string}` + + If `app.Controller("/assets", new(file.Controller))` + + - `func(*Controller) GetByWildard(path string)` - `GET:/assets/{param:path}` + Using Iris MVC for code reuse diff --git a/iris.go b/iris.go index ea53e86e..0e911a61 100644 --- a/iris.go +++ b/iris.go @@ -32,7 +32,7 @@ import ( const ( // Version is the current version number of the Iris Web Framework. - Version = "8.3.4" + Version = "8.4.0" ) // HTTP status codes as registered with IANA. diff --git a/mvc/activator/activator.go b/mvc/activator/activator.go index 2dd7cc5e..af363f4a 100644 --- a/mvc/activator/activator.go +++ b/mvc/activator/activator.go @@ -2,6 +2,11 @@ package activator import ( "reflect" + "strings" + + "github.com/kataras/iris/mvc/activator/methodfunc" + "github.com/kataras/iris/mvc/activator/model" + "github.com/kataras/iris/mvc/activator/persistence" "github.com/kataras/golog" @@ -16,93 +21,23 @@ type ( // think it as a "supervisor" of your Controller which // cares about you. TController struct { + // The name of the front controller struct. + Name string + // FullName it's the last package path segment + "." + the Name. + // i.e: if login-example/user/controller.go, the FullName is "user.Controller". + FullName string // the type of the user/dev's "c" controller (interface{}). Type reflect.Type // it's the first passed value of the controller instance, // we need this to collect and save the persistence fields' values. Value reflect.Value - binder *binder // executed even before the BeginRequest if not nil. - - controls []TControl // executed on request, after the BeginRequest and before the EndRequest. - - // the actual method functions - // i.e for "GET" it's the `Get()` - // - // Here we have a strange relation by-design. - // It contains the methods - // but we have different handlers - // for each of these methods, - // while in the same time all of these - // are depend from this TypeInfo struct. - // So we have TypeInfo -> Methods -> Each(TypeInfo, Method.Index) - // -> Handler for X HTTPMethod, see `Register`. - Methods []MethodFunc - } - // MethodFunc is part of the `TController`, - // it contains the index for a specific http method, - // taken from user's controller struct. - MethodFunc struct { - Index int - HTTPMethod string + binder *binder // executed even before the BeginRequest if not nil. + modelController *model.Controller + persistenceController *persistence.Controller } ) -// ErrControlSkip never shows up, used to determinate -// if a control's Load return error is critical or not, -// `ErrControlSkip` means that activation can continue -// and skip this control. -var ErrControlSkip = errors.New("skip control") - -// TControl is an optional feature that an app can benefit -// by using its own custom controls to control the flow -// inside a controller, they are being registered per controller. -// -// Naming: -// I could find better name such as 'Control', -// but I can imagine the user's confusion about `Controller` -// and `Control` types, they are different but they may -// use that as embedded, so it can not start with the world "C..". -// The best name that shows the relation between this -// and the controller type info struct(TController) is the "TControl", -// `TController` is prepended with "T" for the same reasons, it's different -// than `Controller`, the TController is the "description" of the user's -// `Controller` embedded field. -type TControl interface { // or CoreControl? - // Load should returns nil if its `Handle` - // should be called on serve time. - // - // if error is filled then controller info - // is not created and that error is returned to the - // high-level caller, but the `ErrControlSkip` can be used - // to skip the control without breaking the rest of the registration. - Load(t *TController) error - // Handle executes the control. - // It accepts the context, the new controller instance - // and the specific methodFunc based on the request. - Handle(ctx context.Context, controller reflect.Value, methodFunc func()) -} - -func isControlErr(err error) bool { - if err != nil { - if isSkipper(err) { - return false - } - return true - } - - return false -} - -func isSkipper(err error) bool { - if err != nil { - if err.Error() == ErrControlSkip.Error() { - return true - } - } - return false -} - // the parent package should complete this "interface" // it's not exported, so their functions // but reflect doesn't care about it, so we are ok @@ -129,10 +64,7 @@ type BaseController interface { } // ActivateController returns a new controller type info description. -// A TController is not useful for the end-developer -// but it can be used for debugging. -func ActivateController(base BaseController, bindValues []interface{}, - controls []TControl) (TController, error) { +func ActivateController(base BaseController, bindValues []interface{}) (TController, error) { // get and save the type. typ := reflect.TypeOf(base) @@ -146,129 +78,73 @@ func ActivateController(base BaseController, bindValues []interface{}, // values later on. val := reflect.Indirect(reflect.ValueOf(base)) ctrlName := val.Type().Name() + pkgPath := val.Type().PkgPath() + fullName := pkgPath[strings.LastIndexByte(pkgPath, '/')+1:] + "." + ctrlName // set the binder, can be nil this check at made at runtime. binder := newBinder(typ.Elem(), bindValues) if binder != nil { for _, bf := range binder.fields { golog.Debugf("MVC %s: binder loaded for '%s' with value:\n%#v", - ctrlName, bf.getFullName(), bf.getValue()) + fullName, bf.GetFullName(), bf.GetValue()) } } t := TController{ - Type: typ, - Value: val, - binder: binder, - } - - // first the custom controls, - // after these, the persistence, - // the method control - // which can set the model and - // last the model control. - controls = append(controls, []TControl{ - // PersistenceDataControl stores the optional data - // that will be shared among all requests. - PersistenceDataControl(), - // MethodControl is the actual method function - // i.e for "GET" it's the `Get()` that will be - // fired. - MethodControl(), - // ModelControl stores the optional models from - // the struct's fields values that - // are being setted by the method function - // and set them as ViewData. - ModelControl()}...) - - for _, control := range controls { - err := control.Load(&t) - // fail on first control error if not ErrControlSkip. - if isControlErr(err) { - return t, err - } - - if isSkipper(err) { - continue - } - - golog.Debugf("MVC %s: succeed load of the %#v", ctrlName, control) - t.controls = append(t.controls, control) + Name: ctrlName, + FullName: fullName, + Type: typ, + Value: val, + binder: binder, + modelController: model.Load(typ), + persistenceController: persistence.Load(typ, val), } return t, nil } -// builds the handler for a type based on the method index (i.e Get() -> [0], Post() -> [1]). -func buildMethodHandler(t TController, methodFuncIndex int) context.Handler { - elem := t.Type.Elem() - ctrlName := t.Value.Type().Name() - /* - // good idea, it speeds up the whole thing by ~1MB per 20MB at my personal - // laptop but this way the Model for example which is not a persistence - // variable can stay for the next request - // (if pointer receiver but if not then variables like `Tmpl` cannot stay) - // and that will have unexpected results. - // however we keep it here I want to see it every day in order to find a better way. +// HandlerOf builds the handler for a type based on the specific method func. +func (t TController) HandlerOf(methodFunc methodfunc.MethodFunc) context.Handler { + var ( + // shared, per-controller + elem = t.Type.Elem() + ctrlName = t.Name - type runtimeC struct { - method func() - c reflect.Value - elem reflect.Value - b BaseController - } - - pool := sync.Pool{ - New: func() interface{} { - - c := reflect.New(elem) - methodFunc := c.Method(methodFuncIndex).Interface().(func()) - b, _ := c.Interface().(BaseController) - - elem := c.Elem() - if t.binder != nil { - t.binder.handle(elem) - } - - rc := runtimeC{ - c: c, - elem: elem, - b: b, - method: methodFunc, - } - return rc - }, - } - */ + hasPersistenceData = t.persistenceController != nil + hasModels = t.modelController != nil + // per-handler + handleRequest = methodFunc.MethodCall + ) return func(ctx context.Context) { - // // create a new controller instance of that type(>ptr). + // create a new controller instance of that type(>ptr). c := reflect.New(elem) - if t.binder != nil { t.binder.handle(c) - if ctx.IsStopped() { - return - } } - // get the Controller embedded field's addr. - // it should never be invalid here because we made that checks on activation. - // but if somone tries to "crack" that, then just stop the world in order to be notified, - // we don't want to go away from that type of mistake. b := c.Interface().(BaseController) b.SetName(ctrlName) + // if has persistence data then set them + // before the end-developer's handler in order to be available there. + if hasPersistenceData { + t.persistenceController.Handle(c) + } + // init the request. b.BeginRequest(ctx) + if ctx.IsStopped() { + return + } - methodFunc := c.Method(methodFuncIndex).Interface().(func()) - // execute the controls by order, including the method control. - for _, control := range t.controls { - if ctx.IsStopped() { - break - } - control.Handle(ctx, c, methodFunc) + // the most important, execute the specific function + // from the controller that is responsible to handle + // this request, by method and path. + handleRequest(ctx, c.Method(methodFunc.Index).Interface()) + // if had models, set them after the end-developer's handler. + if hasModels { + t.modelController.Handle(ctx, c) } // finally, execute the controller, don't check for IsStopped. @@ -277,7 +153,7 @@ func buildMethodHandler(t TController, methodFuncIndex int) context.Handler { } // RegisterFunc used by the caller to register the result routes. -type RegisterFunc func(httpMethod string, handler ...context.Handler) +type RegisterFunc func(relPath string, httpMethod string, handler ...context.Handler) // RegisterMethodHandlers receives a `TController`, description of the // user's controller, and calls the "registerFunc" for each of its @@ -286,37 +162,47 @@ type RegisterFunc func(httpMethod string, handler ...context.Handler) // Not useful for the end-developer, but may needed for debugging // at the future. func RegisterMethodHandlers(t TController, registerFunc RegisterFunc) { + var middleware context.Handlers + + if t.binder != nil { + if m := t.binder.middleware; len(m) > 0 { + middleware = m + } + } + // the actual method functions + // i.e for "GET" it's the `Get()`. + methods := methodfunc.Resolve(t.Type) + // range over the type info's method funcs, // build a new handler for each of these // methods and register them to their // http methods using the registerFunc, which is // responsible to convert these into routes // and add them to router via the APIBuilder. - - var handlers context.Handlers - - if t.binder != nil { - if m := t.binder.middleware; len(m) > 0 { - handlers = append(handlers, t.binder.middleware...) + for _, m := range methods { + h := t.HandlerOf(m) + if h == nil { + golog.Debugf("MVC %s: nil method handler found for %s", t.FullName, m.Name) + continue } - } + registeredHandlers := append(middleware, h) + registerFunc(m.RelPath, m.HTTPMethod, registeredHandlers...) - for _, m := range t.Methods { - methodHandler := buildMethodHandler(t, m.Index) - registeredHandlers := append(handlers, methodHandler) - registerFunc(m.HTTPMethod, registeredHandlers...) + golog.Debugf("MVC %s: %s %s maps to function[%d] '%s'", t.FullName, + m.HTTPMethod, + m.RelPath, + m.Index, + m.Name) } } // Register receives a "controller", // a pointer of an instance which embeds the `Controller`, -// the value of "baseControllerFieldName" should be `Controller` -// if embedded and "controls" that can intercept on controller -// activation and on the controller's handler, at serve-time. -func Register(controller BaseController, bindValues []interface{}, controls []TControl, +// the value of "baseControllerFieldName" should be `Controller`. +func Register(controller BaseController, bindValues []interface{}, registerFunc RegisterFunc) error { - t, err := ActivateController(controller, bindValues, controls) + t, err := ActivateController(controller, bindValues) if err != nil { return err } diff --git a/mvc/activator/binder.go b/mvc/activator/binder.go index 95469e5e..edf99d6f 100644 --- a/mvc/activator/binder.go +++ b/mvc/activator/binder.go @@ -3,12 +3,14 @@ package activator import ( "reflect" + "github.com/kataras/iris/mvc/activator/field" + "github.com/kataras/iris/context" ) type binder struct { values []interface{} - fields []field + fields []field.Field // saves any middleware that may need to be passed to the router, // statically, to gain performance. @@ -53,7 +55,7 @@ func (b *binder) storeValueIfMiddleware(value reflect.Value) bool { return false } -func (b *binder) lookup(elem reflect.Type) (fields []field) { +func (b *binder) lookup(elem reflect.Type) (fields []field.Field) { for _, v := range b.values { value := reflect.ValueOf(v) // handlers will be recognised as middleware, not struct fields. @@ -70,11 +72,11 @@ func (b *binder) lookup(elem reflect.Type) (fields []field) { return elemField.Type == value.Type() } - handler := func(f *field) { + handler := func(f *field.Field) { f.Value = value } - fields = append(fields, lookupFields(elem, matcher, handler)...) + fields = append(fields, field.LookupFields(elem, matcher, handler)...) } return } @@ -89,6 +91,6 @@ func (b *binder) handle(c reflect.Value) { elem := c.Elem() // controller should always be a pointer at this state for _, f := range b.fields { - f.sendTo(elem) + f.SendTo(elem) } } diff --git a/mvc/activator/callable_control.go b/mvc/activator/callable_control.go deleted file mode 100644 index 6d4d1c74..00000000 --- a/mvc/activator/callable_control.go +++ /dev/null @@ -1,53 +0,0 @@ -package activator - -import ( - "reflect" - - "github.com/kataras/iris/context" -) - -func getCustomFuncIndex(t *TController, funcNames ...string) (funcIndex int, has bool) { - val := t.Value - - for _, funcName := range funcNames { - if m, has := t.Type.MethodByName(funcName); has { - if _, isRequestFunc := val.Method(m.Index).Interface().(func(ctx context.Context)); isRequestFunc { - return m.Index, has - } - } - } - - return -1, false -} - -type callableControl struct { - Functions []string - index int -} - -func (cc *callableControl) Load(t *TController) error { - funcIndex, has := getCustomFuncIndex(t, cc.Functions...) - if !has { - return ErrControlSkip - } - - cc.index = funcIndex - return nil -} - -// the "c" is a new "c" instance -// which is being used at serve time, inside the Handler. -// it calls the custom function (can be "Init", "BeginRequest", "End" and "EndRequest"), -// the check of this function made at build time, so it's a safe a call. -func (cc *callableControl) Handle(ctx context.Context, c reflect.Value, methodFunc func()) { - c.Method(cc.index).Interface().(func(ctx context.Context))(ctx) -} - -// CallableControl is a generic-propose `TControl` -// which finds one function in the user's controller's struct -// based on the possible "funcName(s)" and executes -// that inside the handler, at serve-time, by passing -// the current request's `iris/context/#Context`. -func CallableControl(funcName ...string) TControl { - return &callableControl{Functions: funcName} -} diff --git a/mvc/activator/field.go b/mvc/activator/field/field.go similarity index 77% rename from mvc/activator/field.go rename to mvc/activator/field/field.go index 7e22d527..ecce0763 100644 --- a/mvc/activator/field.go +++ b/mvc/activator/field/field.go @@ -1,10 +1,13 @@ -package activator +package field import ( "reflect" ) -type field struct { +// Field is a controller's field +// contains all the necessary, internal, information +// to work with. +type Field struct { Name string // the field's original name // but if a tag with `name: "other"` // exist then this fill is filled, otherwise it's the same as the Name. @@ -13,55 +16,55 @@ type field struct { Type reflect.Type Value reflect.Value - embedded *field + embedded *Field } -// getIndex returns all the "dimensions" +// GetIndex returns all the "dimensions" // of the controller struct field's position that this field is referring to, // recursively. // Usage: elem.FieldByIndex(field.getIndex()) // for example the {0,1} means that the field is on the second field of the first's // field of this struct. -func (ff field) getIndex() []int { +func (ff Field) GetIndex() []int { deepIndex := []int{ff.Index} if emb := ff.embedded; emb != nil { - deepIndex = append(deepIndex, emb.getIndex()...) + deepIndex = append(deepIndex, emb.GetIndex()...) } return deepIndex } -// getType returns the type of the referring field, recursively. -func (ff field) getType() reflect.Type { +// GetType returns the type of the referring field, recursively. +func (ff Field) GetType() reflect.Type { typ := ff.Type if emb := ff.embedded; emb != nil { - return emb.getType() + return emb.GetType() } return typ } -// getFullName returns the full name of that field +// GetFullName returns the full name of that field // i.e: UserController.SessionController.Manager, // it's useful for debugging only. -func (ff field) getFullName() string { +func (ff Field) GetFullName() string { name := ff.Name if emb := ff.embedded; emb != nil { - return name + "." + emb.getFullName() + return name + "." + emb.GetFullName() } return name } -// getTagName returns the tag name of the referring field +// GetTagName returns the tag name of the referring field // recursively. -func (ff field) getTagName() string { +func (ff Field) GetTagName() string { name := ff.TagName if emb := ff.embedded; emb != nil { - return emb.getTagName() + return emb.GetTagName() } return name @@ -74,10 +77,10 @@ func checkVal(val reflect.Value) bool { return val.IsValid() && (val.Kind() == reflect.Ptr && !val.IsNil()) && val.CanInterface() } -// getValue returns a valid value of the referring field, recursively. -func (ff field) getValue() interface{} { +// GetValue returns a valid value of the referring field, recursively. +func (ff Field) GetValue() interface{} { if ff.embedded != nil { - return ff.embedded.getValue() + return ff.embedded.GetValue() } if checkVal(ff.Value) { @@ -87,17 +90,17 @@ func (ff field) getValue() interface{} { return "undefinied value" } -// sendTo should be used when this field or its embedded +// SendTo should be used when this field or its embedded // has a Value on it. // It sets the field's value to the "elem" instance, it's the new controller. -func (ff field) sendTo(elem reflect.Value) { +func (ff Field) SendTo(elem reflect.Value) { // note: // we don't use the getters here // because we do recursively search by our own here // to be easier to debug if ever needed. if embedded := ff.embedded; embedded != nil { if ff.Index >= 0 { - embedded.sendTo(elem.Field(ff.Index)) + embedded.SendTo(elem.Field(ff.Index)) } return } @@ -121,18 +124,18 @@ func lookupTagName(elemField reflect.StructField) string { return vname } -// lookupFields iterates all "elem"'s fields and its fields +// LookupFields iterates all "elem"'s fields and its fields // if structs, recursively. // Compares them to the "matcher", if they passed // then it executes the "handler" if any, // the handler can change the field as it wants to. // // It finally returns that collection of the valid fields, can be empty. -func lookupFields(elem reflect.Type, matcher func(reflect.StructField) bool, handler func(*field)) (fields []field) { +func LookupFields(elem reflect.Type, matcher func(reflect.StructField) bool, handler func(*Field)) (fields []Field) { for i, n := 0, elem.NumField(); i < n; i++ { elemField := elem.Field(i) if matcher(elemField) { - field := field{ + field := Field{ Index: i, Name: elemField.Name, TagName: lookupTagName(elemField), @@ -150,7 +153,7 @@ func lookupFields(elem reflect.Type, matcher func(reflect.StructField) bool, han f := lookupStructField(elemField.Type, matcher, handler) if f != nil { - fields = append(fields, field{ + fields = append(fields, Field{ Index: i, Name: elemField.Name, Type: elemField.Type, @@ -168,7 +171,7 @@ func lookupFields(elem reflect.Type, matcher func(reflect.StructField) bool, han // for both structured (embedded) fields and normal fields // but we keep that as it's, a new function like this // is easier for debugging, if ever needed. -func lookupStructField(elem reflect.Type, matcher func(reflect.StructField) bool, handler func(*field)) *field { +func lookupStructField(elem reflect.Type, matcher func(reflect.StructField) bool, handler func(*Field)) *Field { // fmt.Printf("lookup struct for elem: %s\n", elem.Name()) // ignore if that field is not a struct @@ -181,7 +184,7 @@ func lookupStructField(elem reflect.Type, matcher func(reflect.StructField) bool elemField := elem.Field(i) if matcher(elemField) { // we area inside the correct type. - f := &field{ + f := &Field{ Index: i, Name: elemField.Name, TagName: lookupTagName(elemField), @@ -202,7 +205,7 @@ func lookupStructField(elem reflect.Type, matcher func(reflect.StructField) bool elemFieldEmb := elem.Field(i) f := lookupStructField(elemFieldEmb.Type, matcher, handler) if f != nil { - fp := &field{ + fp := &Field{ Index: i, Name: elemFieldEmb.Name, TagName: lookupTagName(elemFieldEmb), diff --git a/mvc/activator/method_control.go b/mvc/activator/method_control.go deleted file mode 100644 index 45520224..00000000 --- a/mvc/activator/method_control.go +++ /dev/null @@ -1,81 +0,0 @@ -package activator - -import ( - "reflect" - "strings" - - "github.com/kataras/iris/context" - "github.com/kataras/iris/core/errors" -) - -var availableMethods = [...]string{ - "ANY", // will be registered using the `core/router#APIBuilder#Any` - "ALL", // same as ANY - "NONE", // offline route - // valid http methods - "GET", - "POST", - "PUT", - "DELETE", - "CONNECT", - "HEAD", - "PATCH", - "OPTIONS", - "TRACE", -} - -type methodControl struct{} - -// ErrMissingHTTPMethodFunc fired when the controller doesn't handle any valid HTTP method. -var ErrMissingHTTPMethodFunc = errors.New(`controller can not be activated, - missing a compatible HTTP method function, i.e Get()`) - -func (mc *methodControl) Load(t *TController) error { - // search the entire controller - // for any compatible method function - // and register that. - for _, method := range availableMethods { - if m, ok := t.Type.MethodByName(getMethodName(method)); ok { - - t.Methods = append(t.Methods, MethodFunc{ - HTTPMethod: method, - Index: m.Index, - }) - - // check if method was Any() or All() - // if yes, then break to skip any conflict with the rest of the method functions. - // (this will be registered to all valid http methods by the APIBuilder) - if method == "ANY" || method == "ALL" { - break - } - } - } - - if len(t.Methods) == 0 { - // no compatible method found, fire an error and stop everything. - return ErrMissingHTTPMethodFunc - } - - return nil -} - -func getMethodName(httpMethod string) string { - httpMethodFuncName := strings.Title(strings.ToLower(httpMethod)) - return httpMethodFuncName -} - -func (mc *methodControl) Handle(ctx context.Context, c reflect.Value, methodFunc func()) { - // execute the responsible method for that handler. - // 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() -} - -// MethodControl loads and serve the main functionality of the controllers, -// which is to run a function based on the http method (pre-computed). -func MethodControl() TControl { - return &methodControl{} -} diff --git a/mvc/activator/methodfunc/func_caller.go b/mvc/activator/methodfunc/func_caller.go new file mode 100644 index 00000000..2ceaa4ac --- /dev/null +++ b/mvc/activator/methodfunc/func_caller.go @@ -0,0 +1,61 @@ +package methodfunc + +import ( + "github.com/kataras/iris/context" +) + +// FuncCaller is responsible to call the controller's function +// which is responsible +// for that request for this http method. +type FuncCaller interface { + // MethodCall fires the actual handler. + // The "ctx" is the current context, helps us to get any path parameter's values. + // + // The "f" is the controller's function which is responsible + // for that request for this http method. + // That function can accept one parameter. + // + // The default callers (and the only one for now) + // are pre-calculated by the framework. + MethodCall(ctx context.Context, f interface{}) +} + +type callerFunc func(ctx context.Context, f interface{}) + +func (c callerFunc) MethodCall(ctx context.Context, f interface{}) { + c(ctx, f) +} + +func resolveCaller(p pathInfo) callerFunc { + // if it's standard `Get`, `Post` without parameters. + if p.ParamType == "" { + return func(ctx context.Context, f interface{}) { + f.(func())() + } + } + + // remember, + // the router already checks for the correct type, + // we did pre-calculate everything + // and now we will pre-calculate the method caller itself as well. + + if p.ParamType == paramTypeInt { + return func(ctx context.Context, f interface{}) { + paramValue, _ := ctx.Params().GetInt(paramName) + f.(func(int))(paramValue) + } + } + + if p.ParamType == paramTypeLong { + return func(ctx context.Context, f interface{}) { + paramValue, _ := ctx.Params().GetInt64(paramName) + f.(func(int64))(paramValue) + } + } + + // else it's string or path, both of them are simple strings. + return func(ctx context.Context, f interface{}) { + paramValue := ctx.Params().Get(paramName) + f.(func(string))(paramValue) + } +} diff --git a/mvc/activator/methodfunc/func_info.go b/mvc/activator/methodfunc/func_info.go new file mode 100644 index 00000000..50269044 --- /dev/null +++ b/mvc/activator/methodfunc/func_info.go @@ -0,0 +1,92 @@ +package methodfunc + +import ( + "reflect" + "strings" + "unicode" +) + +var availableMethods = [...]string{ + "ANY", // will be registered using the `core/router#APIBuilder#Any` + "ALL", // same as ANY + "NONE", // offline route + // valid http methods + "GET", + "POST", + "PUT", + "DELETE", + "CONNECT", + "HEAD", + "PATCH", + "OPTIONS", + "TRACE", +} + +// FuncInfo is part of the `TController`, +// it contains the index for a specific http method, +// taken from user's controller struct. +type FuncInfo struct { + // Name is the map function name. + Name string + // Trailing is not empty when the Name contains + // characters after the titled method, i.e + // if Name = Get -> empty + // if Name = GetLogin -> Login + // if Name = GetUserPost -> UserPost + Trailing string + + // The Type of the method, includes the receivers. + Type reflect.Type + + // Index is the index of this function inside the controller type. + Index int + // HTTPMethod is the original http method that this + // function should be registered to and serve. + // i.e "GET","POST","PUT"... + HTTPMethod string +} + +// or resolve methods +func fetchInfos(typ reflect.Type) (methods []FuncInfo) { + // search the entire controller + // for any compatible method function + // and add that. + + for i, n := 0, typ.NumMethod(); i < n; i++ { + m := typ.Method(i) + name := m.Name + + for _, method := range availableMethods { + possibleMethodFuncName := methodTitle(method) + + if strings.Index(name, possibleMethodFuncName) == 0 { + trailing := "" + // if has chars after the method itself + if lname, lmethod := len(name), len(possibleMethodFuncName); lname > lmethod { + ch := rune(name[lmethod]) + // if the next char is upper, otherise just skip the whole func info. + if unicode.IsUpper(ch) { + trailing = name[lmethod:] + } else { + continue + } + } + + methodInfo := FuncInfo{ + Name: name, + Trailing: trailing, + Type: m.Type, + HTTPMethod: method, + Index: m.Index, + } + methods = append(methods, methodInfo) + } + } + } + return +} + +func methodTitle(httpMethod string) string { + httpMethodFuncName := strings.Title(strings.ToLower(httpMethod)) + return httpMethodFuncName +} diff --git a/mvc/activator/methodfunc/func_path.go b/mvc/activator/methodfunc/func_path.go new file mode 100644 index 00000000..f9e926b1 --- /dev/null +++ b/mvc/activator/methodfunc/func_path.go @@ -0,0 +1,119 @@ +package methodfunc + +import ( + "bytes" + "fmt" + "strings" + "unicode" +) + +const ( + by = "By" + wildcard = "Wildcard" + paramName = "param" +) + +type pathInfo struct { + GoParamType string + ParamType string + RelPath string +} + +const ( + paramTypeInt = "int" + paramTypeLong = "long" + paramTypeString = "string" + paramTypePath = "path" +) + +var macroTypes = map[string]string{ + "int": paramTypeInt, + "int64": paramTypeLong, + "string": paramTypeString, + // there is "path" param type but it's being captured "on-air" + // "file" param type is not supported by the current implementation, yet + // but if someone ask for it I'll implement it, it's easy. +} + +func resolveRelativePath(info FuncInfo) (p pathInfo, ok bool) { + if info.Trailing == "" { + // it's valid + // it's just don't have a relative path, + // therefore p.RelPath will be empty, as we want. + return p, true + } + + var ( + typ = info.Type + tr = info.Trailing + relPath = resolvePathFromFunc(tr) + + goType, paramType string + ) + + byKeywordIdx := strings.LastIndex(tr, by) + if byKeywordIdx != -1 && typ.NumIn() == 2 { // first is the struct receiver. + funcPath := tr[0:byKeywordIdx] // remove the "By" + goType = typ.In(1).Name() + afterBy := byKeywordIdx + len(by) + if len(tr) > afterBy { + if tr[afterBy:] == wildcard { + paramType = paramTypePath + } else { + // invalid syntax + return p, false + } + } else { + // it's not wildcard, so check base on our available macro types. + if paramType, ok = macroTypes[goType]; !ok { + // ivalid type + return p, false + } + } + + // int and string are supported. + // as there is no way to get the parameter name + // we will use the "param" everywhere. + suffix := fmt.Sprintf("/{%s:%s}", paramName, paramType) + relPath = resolvePathFromFunc(funcPath) + suffix + } + + // if GetSomething/PostSomething/PutSomething... + // we will not check for "Something" because we could + // occur unexpected behaviors to the existing users + // who using exported functions for controller's internal + // functionalities and not for serving a request path. + + return pathInfo{ + GoParamType: goType, + ParamType: paramType, + RelPath: relPath, + }, true +} + +func resolvePathFromFunc(funcName string) string { + end := len(funcName) + start := -1 + buf := &bytes.Buffer{} + + for i, n := 0, end; i < n; i++ { + c := rune(funcName[i]) + if unicode.IsUpper(c) { + // it doesn't count the last uppercase + if start != -1 { + end = i + s := "/" + strings.ToLower(funcName[start:end]) + buf.WriteString(s) + } + start = i + continue + } + end = i + 1 + } + + if end > 0 && len(funcName) >= end { + buf.WriteString("/" + strings.ToLower(funcName[start:end])) + } + + return buf.String() +} diff --git a/mvc/activator/methodfunc/methodfunc.go b/mvc/activator/methodfunc/methodfunc.go new file mode 100644 index 00000000..1012355f --- /dev/null +++ b/mvc/activator/methodfunc/methodfunc.go @@ -0,0 +1,35 @@ +package methodfunc + +import ( + "reflect" +) + +// MethodFunc the handler function. +type MethodFunc struct { + FuncInfo + FuncCaller + RelPath string +} + +// Resolve returns all the method funcs +// necessary information and actions to +// perform the request. +func Resolve(typ reflect.Type) (methodFuncs []MethodFunc) { + infos := fetchInfos(typ) + for _, info := range infos { + p, ok := resolveRelativePath(info) + if !ok { + continue + } + caller := resolveCaller(p) + methodFunc := MethodFunc{ + RelPath: p.RelPath, + FuncInfo: info, + FuncCaller: caller, + } + + methodFuncs = append(methodFuncs, methodFunc) + } + + return +} diff --git a/mvc/activator/model_control.go b/mvc/activator/model/model.go similarity index 50% rename from mvc/activator/model_control.go rename to mvc/activator/model/model.go index 8fc3aad1..078cc6d4 100644 --- a/mvc/activator/model_control.go +++ b/mvc/activator/model/model.go @@ -1,16 +1,27 @@ -package activator +package model import ( "reflect" + "github.com/kataras/iris/mvc/activator/field" + "github.com/kataras/iris/context" ) -type modelControl struct { - fields []field +// Controller is responsible +// to load and handle the `Model(s)` inside a controller struct +// via the `iris:"model"` tag field. +// It stores the optional models from +// the struct's fields values that +// are being setted by the method function +// and set them as ViewData. +type Controller struct { + fields []field.Field } -func (mc *modelControl) Load(t *TController) error { +// Load tries to lookup and set for any valid model field. +// Returns nil if no models are being used. +func Load(typ reflect.Type) *Controller { matcher := func(f reflect.StructField) bool { if tag, ok := f.Tag.Lookup("iris"); ok { if tag == "model" { @@ -20,26 +31,27 @@ func (mc *modelControl) Load(t *TController) error { return false } - fields := lookupFields(t.Type.Elem(), matcher, nil) + fields := field.LookupFields(typ.Elem(), matcher, nil) if len(fields) == 0 { - // first is the `Controller` so we need to - // check the second and after that. - return ErrControlSkip + return nil } - mc.fields = fields - return nil + mc := &Controller{ + fields: fields, + } + return mc } -func (mc *modelControl) Handle(ctx context.Context, c reflect.Value, methodFunc func()) { +// Handle transfer the models to the view. +func (mc *Controller) Handle(ctx context.Context, c reflect.Value) { elem := c.Elem() // controller should always be a pointer at this state for _, f := range mc.fields { - index := f.getIndex() - typ := f.getType() - name := f.getTagName() + index := f.GetIndex() + typ := f.GetType() + name := f.GetTagName() elemField := elem.FieldByIndex(index) // check if current controller's element field @@ -52,12 +64,10 @@ func (mc *modelControl) Handle(ctx context.Context, c reflect.Value, methodFunc fieldValue := elemField.Interface() ctx.ViewData(name, fieldValue) + // /*maybe some time in the future*/ if resetable { + // // clean up + // elemField.Set(reflect.Zero(typ)) + // } + } } - -// ModelControl returns a TControl which is responsible -// to load and handle the `Model(s)` inside a controller struct -// via the `iris:"model"` tag field. -func ModelControl() TControl { - return &modelControl{} -} diff --git a/mvc/activator/persistence/persistence.go b/mvc/activator/persistence/persistence.go new file mode 100644 index 00000000..8b81ccbc --- /dev/null +++ b/mvc/activator/persistence/persistence.go @@ -0,0 +1,60 @@ +package persistence + +import ( + "reflect" + + "github.com/kataras/iris/mvc/activator/field" +) + +// Controller is responsible to load from the original +// end-developer's main controller's value +// and re-store the persistence data by scanning the original. +// It stores and sets to each new controller +// the optional data that should be shared among all requests. +type Controller struct { + fields []field.Field +} + +// Load scans and load for persistence data based on the `iris:"persistence"` tag. +// +// The type is the controller's Type. +// the "val" is the original end-developer's controller's Value. +// Returns nil if no persistence data to store found. +func Load(typ reflect.Type, val reflect.Value) *Controller { + matcher := func(elemField reflect.StructField) bool { + if tag, ok := elemField.Tag.Lookup("iris"); ok { + if tag == "persistence" { + return true + } + } + return false + } + + handler := func(f *field.Field) { + valF := val.Field(f.Index) + if valF.IsValid() || (valF.Kind() == reflect.Ptr && !valF.IsNil()) { + val := reflect.ValueOf(valF.Interface()) + if val.IsValid() || (val.Kind() == reflect.Ptr && !val.IsNil()) { + f.Value = val + } + } + } + + fields := field.LookupFields(typ.Elem(), matcher, handler) + + if len(fields) == 0 { + return nil + } + + return &Controller{ + fields: fields, + } +} + +// Handle re-stores the persistence data at the current controller. +func (pc *Controller) Handle(c reflect.Value) { + elem := c.Elem() // controller should always be a pointer at this state + for _, f := range pc.fields { + f.SendTo(elem) + } +} diff --git a/mvc/activator/persistence_data_control.go b/mvc/activator/persistence_data_control.go deleted file mode 100644 index a2d05ec6..00000000 --- a/mvc/activator/persistence_data_control.go +++ /dev/null @@ -1,57 +0,0 @@ -package activator - -import ( - "reflect" - - "github.com/kataras/iris/context" -) - -type persistenceDataControl struct { - fields []field -} - -func (d *persistenceDataControl) Load(t *TController) error { - matcher := func(elemField reflect.StructField) bool { - if tag, ok := elemField.Tag.Lookup("iris"); ok { - if tag == "persistence" { - return true - } - } - return false - } - - handler := func(f *field) { - valF := t.Value.Field(f.Index) - if valF.IsValid() || (valF.Kind() == reflect.Ptr && !valF.IsNil()) { - val := reflect.ValueOf(valF.Interface()) - if val.IsValid() || (val.Kind() == reflect.Ptr && !val.IsNil()) { - f.Value = val - } - } - } - - fields := lookupFields(t.Type.Elem(), matcher, handler) - - if len(fields) == 0 { - // first is the `Controller` so we need to - // check the second and after that. - return ErrControlSkip - } - - d.fields = fields - return nil -} - -func (d *persistenceDataControl) Handle(ctx context.Context, c reflect.Value, methodFunc func()) { - elem := c.Elem() // controller should always be a pointer at this state - for _, f := range d.fields { - f.sendTo(elem) - } -} - -// PersistenceDataControl loads and re-stores -// the persistence data by scanning the original -// `TController.Value` instance of the user's controller. -func PersistenceDataControl() TControl { - return &persistenceDataControl{} -} diff --git a/mvc/controller_test.go b/mvc/controller_test.go index 93412e8d..b6b3814e 100644 --- a/mvc/controller_test.go +++ b/mvc/controller_test.go @@ -436,3 +436,41 @@ func TestControllerInsideControllerRecursively(t *testing.T) { e.GET("/user/" + username).Expect(). Status(httptest.StatusOK).Body().Equal(expected) } + +type testControllerRelPathFromFunc struct{ mvc.Controller } + +func (c *testControllerRelPathFromFunc) EndRequest(ctx context.Context) { + ctx.Writef("%s:%s", ctx.Method(), ctx.Path()) + c.Controller.EndRequest(ctx) +} + +func (c *testControllerRelPathFromFunc) Get() {} +func (c *testControllerRelPathFromFunc) GetLogin() {} +func (c *testControllerRelPathFromFunc) PostLogin() {} + +func (c *testControllerRelPathFromFunc) GetAdminLogin() {} +func (c *testControllerRelPathFromFunc) PutSomethingIntoThis() {} +func (c *testControllerRelPathFromFunc) GetBy(int64) {} + +func (c *testControllerRelPathFromFunc) GetByWildcard(string) {} + +func TestControllerRelPathFromFunc(t *testing.T) { + app := iris.New() + app.Controller("/", new(testControllerRelPathFromFunc)) + + e := httptest.New(t, app) + e.GET("/").Expect().Status(httptest.StatusOK). + Body().Equal("GET:/") + e.GET("/login").Expect().Status(httptest.StatusOK). + Body().Equal("GET:/login") + e.POST("/login").Expect().Status(httptest.StatusOK). + Body().Equal("POST:/login") + e.GET("/admin/login").Expect().Status(httptest.StatusOK). + Body().Equal("GET:/admin/login") + e.PUT("/something/into/this").Expect().Status(httptest.StatusOK). + Body().Equal("PUT:/something/into/this") + e.GET("/42").Expect().Status(httptest.StatusOK). + Body().Equal("GET:/42") + e.GET("/anything/here").Expect().Status(httptest.StatusOK). + Body().Equal("GET:/anything/here") +}