Update to 8.4.0 | New macro type, new high-optimized MVC features. Read HISTORY.md

Former-commit-id: b72a23ba063be60a9750c8b1b0df024b0c8ed549
This commit is contained in:
kataras 2017-08-27 18:46:04 +03:00
parent 8602517371
commit 591806795e
37 changed files with 1242 additions and 453 deletions

View File

@ -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.

View File

@ -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)

View File

@ -1 +1 @@
8.3.4:https://github.com/kataras/iris/blob/master/HISTORY.md#we-23-august-2017--v834
8.4.0:https://github.com/kataras/iris/blob/master/HISTORY.md#su-27-august-2017--v840

View File

@ -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

View File

@ -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)
}

View File

@ -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"),
)
}

View File

@ -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%;
}
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -0,0 +1,4 @@
<h1>Error.</h1>
<h2>An error occurred while processing your request.</h2>
<h3>{{.Message}}</h3>

View File

@ -0,0 +1,12 @@
<html>
<head>
<title>{{.Title}}</title>
<link rel="stylesheet" type="text/css" href="/public/css/site.css" />
</head>
<body>
{{ yield }}
</body>
</html>

View File

@ -0,0 +1,11 @@
<form action="/user/login" method="POST">
<div class="container">
<label><b>Username</b></label>
<input type="text" placeholder="Enter Username" name="username" required>
<label><b>Password</b></label>
<input type="password" placeholder="Enter Password" name="password" required>
<button type="submit">Login</button>
</div>
</form>

View File

@ -0,0 +1,3 @@
<p>
Welcome back <strong>{{.User.Firstname}}</strong>!
</p>

View File

@ -0,0 +1,3 @@
<p>
User with ID <strong>{{.ID}}</strong> does not exist.
</p>

View File

@ -0,0 +1,14 @@
<form action="/user/register" method="POST">
<div class="container">
<label><b>Firstname</b></label>
<input type="text" placeholder="Enter Firstname" name="firstname" required>
<label><b>Username</b></label>
<input type="text" placeholder="Enter Username" name="username" required>
<label><b>Password</b></label>
<input type="password" placeholder="Enter Password" name="password" required>
<button type="submit">Register</button>
</div>
</form>

View File

@ -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)
}

View File

@ -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)

View File

@ -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,

View File

@ -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
}

View File

@ -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:

23
doc.go
View File

@ -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

View File

@ -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.

View File

@ -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,6 +21,11 @@ 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,
@ -23,86 +33,11 @@ type (
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
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{
Name: ctrlName,
FullName: fullName,
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)
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)
methodFunc := c.Method(methodFuncIndex).Interface().(func())
// execute the controls by order, including the method control.
for _, control := range t.controls {
if ctx.IsStopped() {
break
return
}
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
}

View File

@ -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)
}
}

View File

@ -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}
}

View File

@ -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),

View File

@ -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{}
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -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
}

View File

@ -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{}
}

View File

@ -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)
}
}

View File

@ -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{}
}

View File

@ -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")
}