remove the old 'mvc' folder - examples are not changed yet - add the 'di' package inside the mvc2 package - which will be renamed to 'mvc' on the next commit - new mvc.Application and some dublications removed - The new version will be version 9 because it will contain breaking changes (not to the end-developer's controllers but to the API they register them) - get ready for 'Christmas Edition' for believers

Former-commit-id: c7114233dee90ee308c0a3e77ec2ad0c361094b8
This commit is contained in:
Gerasimos (Makis) Maropoulos 2017-12-15 20:28:06 +02:00
parent 4e15f4ea88
commit 55dfd195e0
48 changed files with 984 additions and 3591 deletions

View File

@ -10,17 +10,19 @@ import (
// TodoController is our TODO app's web controller. // TodoController is our TODO app's web controller.
type TodoController struct { type TodoController struct {
service todo.Service Service todo.Service
session *sessions.Session Session *sessions.Session
} }
// OnActivate called once before the server ran, can bind custom // BeforeActivate called once before the server ran, and before
// things to the controller. // the routes and dependency binder builded.
func (c *TodoController) OnActivate(ca *mvc.ControllerActivator) { // You can bind custom things to the controller, add new methods, add middleware,
// add dependencies to the struct or the method(s) and more.
func (c *TodoController) BeforeActivate(ca *mvc.ControllerActivator) {
// this could be binded to a controller's function input argument // this could be binded to a controller's function input argument
// if any, or struct field if any: // if any, or struct field if any:
ca.Bind(func(ctx iris.Context) todo.Item { ca.Dependencies.Add(func(ctx iris.Context) todo.Item {
// ctx.ReadForm(&item) // ctx.ReadForm(&item)
var ( var (
owner = ctx.PostValue("owner") owner = ctx.PostValue("owner")
@ -35,25 +37,30 @@ func (c *TodoController) OnActivate(ca *mvc.ControllerActivator) {
} }
}) })
// ca.Router.Use(...).Done(...).Layout(...)
// TODO:(?)
// m := ca.Method("PutCompleteBy")
// m.Route.Use(...).Done(...) <- we don't have the route here but I can find something to solve this.
// m.Dependencies.Add(...)
} }
// Get handles the GET: /todo route. // Get handles the GET: /todo route.
func (c *TodoController) Get() []todo.Item { func (c *TodoController) Get() []todo.Item {
return c.service.GetByOwner(c.session.ID()) return c.Service.GetByOwner(c.Session.ID())
} }
// PutCompleteBy handles the PUT: /todo/complete/{id:long} route. // PutCompleteBy handles the PUT: /todo/complete/{id:long} route.
func (c *TodoController) PutCompleteBy(id int64) int { func (c *TodoController) PutCompleteBy(id int64) int {
item, found := c.service.GetByID(id) item, found := c.Service.GetByID(id)
if !found { if !found {
return iris.StatusNotFound return iris.StatusNotFound
} }
if item.OwnerID != c.session.ID() { if item.OwnerID != c.Session.ID() {
return iris.StatusForbidden return iris.StatusForbidden
} }
if !c.service.Complete(item) { if !c.Service.Complete(item) {
return iris.StatusBadRequest return iris.StatusBadRequest
} }
@ -62,11 +69,11 @@ func (c *TodoController) PutCompleteBy(id int64) int {
// Post handles the POST: /todo route. // Post handles the POST: /todo route.
func (c *TodoController) Post(newItem todo.Item) int { func (c *TodoController) Post(newItem todo.Item) int {
if newItem.OwnerID != c.session.ID() { if newItem.OwnerID != c.Session.ID() {
return iris.StatusForbidden return iris.StatusForbidden
} }
if err := c.service.Save(newItem); err != nil { if err := c.Service.Save(newItem); err != nil {
return iris.StatusBadRequest return iris.StatusBadRequest
} }
return iris.StatusOK return iris.StatusOK

View File

@ -10,7 +10,6 @@ import (
"github.com/kataras/iris/context" "github.com/kataras/iris/context"
"github.com/kataras/iris/core/errors" "github.com/kataras/iris/core/errors"
"github.com/kataras/iris/core/router/macro" "github.com/kataras/iris/core/router/macro"
"github.com/kataras/iris/mvc/activator"
) )
const ( const (
@ -478,85 +477,6 @@ func (api *APIBuilder) Any(relativePath string, handlers ...context.Handler) (ro
return return
} }
// Controller registers a `Controller` instance and returns the registered Routes.
// The "controller" receiver should embed a field of `Controller` in order
// to be compatible Iris `Controller`.
//
// It's just an alternative way of building an API for a specific
// path, the controller can register all type of http methods.
//
// Keep note that controllers are bit slow
// because of the reflection use however it's as fast as possible because
// it does preparation before the serve-time handler but still
// remains slower than the low-level handlers
// such as `Handle, Get, Post, Put, Delete, Connect, Head, Trace, Patch`.
//
//
// All fields that are tagged with iris:"persistence"` or binded
// are being persistence and kept the same between the different requests.
//
// An Example Controller can be:
//
// type IndexController struct {
// Controller
// }
//
// func (c *IndexController) Get() {
// c.Tmpl = "index.html"
// c.Data["title"] = "Index page"
// c.Data["message"] = "Hello world!"
// }
//
// Usage: app.Controller("/", new(IndexController))
//
//
// Another example with bind:
//
// type UserController struct {
// Controller
//
// DB *DB
// CreatedAt time.Time
//
// }
//
// // Get serves using the User controller when HTTP Method is "GET".
// func (c *UserController) Get() {
// c.Tmpl = "user/index.html"
// c.Data["title"] = "User Page"
// c.Data["username"] = "kataras " + c.Params.Get("userid")
// c.Data["connstring"] = c.DB.Connstring
// c.Data["uptime"] = time.Now().Sub(c.CreatedAt).Seconds()
// }
//
// Usage: app.Controller("/user/{id:int}", new(UserController), db, time.Now())
// Note: Binded values of context.Handler type are being recognised as middlewares by the router.
//
// Read more at `/mvc#Controller`.
func (api *APIBuilder) Controller(relativePath string, controller activator.BaseController,
bindValues ...interface{}) (routes []*Route) {
registerFunc := func(method string, ifRelPath 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
// and set them on each new request controller,
// binder is an alternative method
// of the persistence data control which requires the
// user already set the values manually to controller's fields
// and tag them with `iris:"persistence"`.
//
// don't worry it will never be handled if empty values.
if err := activator.Register(controller, bindValues, registerFunc); err != nil {
api.reporter.Add("%v for path: '%s'", err, relativePath)
}
return
}
// StaticCacheDuration expiration duration for INACTIVE file handlers, it's the only one global configuration // StaticCacheDuration expiration duration for INACTIVE file handlers, it's the only one global configuration
// which can be changed. // which can be changed.
var StaticCacheDuration = 20 * time.Second var StaticCacheDuration = 20 * time.Second

View File

@ -4,7 +4,6 @@ import (
"github.com/kataras/iris/context" "github.com/kataras/iris/context"
"github.com/kataras/iris/core/errors" "github.com/kataras/iris/core/errors"
"github.com/kataras/iris/core/router/macro" "github.com/kataras/iris/core/router/macro"
"github.com/kataras/iris/mvc/activator"
) )
// Party is here to separate the concept of // Party is here to separate the concept of
@ -130,63 +129,6 @@ type Party interface {
// (Get,Post,Put,Head,Patch,Options,Connect,Delete). // (Get,Post,Put,Head,Patch,Options,Connect,Delete).
Any(registeredPath string, handlers ...context.Handler) []*Route Any(registeredPath string, handlers ...context.Handler) []*Route
// Controller registers a `Controller` instance and returns the registered Routes.
// The "controller" receiver should embed a field of `Controller` in order
// to be compatible Iris `Controller`.
//
// It's just an alternative way of building an API for a specific
// path, the controller can register all type of http methods.
//
// Keep note that controllers are bit slow
// because of the reflection use however it's as fast as possible because
// it does preparation before the serve-time handler but still
// remains slower than the low-level handlers
// such as `Handle, Get, Post, Put, Delete, Connect, Head, Trace, Patch`.
//
//
// All fields that are tagged with iris:"persistence"` or binded
// are being persistence and kept the same between the different requests.
//
// An Example Controller can be:
//
// type IndexController struct {
// Controller
// }
//
// func (c *IndexController) Get() {
// c.Tmpl = "index.html"
// c.Data["title"] = "Index page"
// c.Data["message"] = "Hello world!"
// }
//
// Usage: app.Controller("/", new(IndexController))
//
//
// Another example with bind:
//
// type UserController struct {
// Controller
//
// DB *DB
// CreatedAt time.Time
//
// }
//
// // Get serves using the User controller when HTTP Method is "GET".
// func (c *UserController) Get() {
// c.Tmpl = "user/index.html"
// c.Data["title"] = "User Page"
// c.Data["username"] = "kataras " + c.Params.Get("userid")
// c.Data["connstring"] = c.DB.Connstring
// c.Data["uptime"] = time.Now().Sub(c.CreatedAt).Seconds()
// }
//
// Usage: app.Controller("/user/{id:int}", new(UserController), db, time.Now())
// Note: Binded values of context.Handler type are being recognised as middlewares by the router.
//
// Read more at `/mvc#Controller`.
Controller(relativePath string, controller activator.BaseController, bindValues ...interface{}) []*Route
// StaticHandler returns a new Handler which is ready // StaticHandler returns a new Handler which is ready
// to serve all kind of static files. // to serve all kind of static files.
// //

129
go19.go
View File

@ -6,7 +6,6 @@ import (
"github.com/kataras/iris/context" "github.com/kataras/iris/context"
"github.com/kataras/iris/core/host" "github.com/kataras/iris/core/host"
"github.com/kataras/iris/core/router" "github.com/kataras/iris/core/router"
"github.com/kataras/iris/mvc"
) )
type ( type (
@ -48,132 +47,4 @@ type (
// //
// A shortcut for the `core/router#Party`, useful when `PartyFunc` is being used. // A shortcut for the `core/router#Party`, useful when `PartyFunc` is being used.
Party = router.Party Party = router.Party
// Controller is the base controller for the high level controllers instances.
//
// This base controller is used as an alternative way of building
// APIs, the controller can register all type of http methods.
//
// Keep note that controllers are bit slow
// because of the reflection use however it's as fast as possible because
// it does preparation before the serve-time handler but still
// remains slower than the low-level handlers
// such as `Handle, Get, Post, Put, Delete, Connect, Head, Trace, Patch`.
//
//
// All fields that are tagged with iris:"persistence"`
// are being persistence and kept between the different requests,
// meaning that these data will not be reset-ed on each new request,
// they will be the same for all requests.
//
// An Example Controller can be:
//
// type IndexController struct {
// iris.Controller
// }
//
// func (c *IndexController) Get() {
// c.Tmpl = "index.html"
// c.Data["title"] = "Index page"
// c.Data["message"] = "Hello world!"
// }
//
// Usage: app.Controller("/", new(IndexController))
//
//
// Another example with persistence data:
//
// type UserController struct {
// iris.Controller
//
// CreatedAt time.Time `iris:"persistence"`
// Title string `iris:"persistence"`
// DB *DB `iris:"persistence"`
// }
//
// // Get serves using the User controller when HTTP Method is "GET".
// func (c *UserController) Get() {
// c.Tmpl = "user/index.html"
// c.Data["title"] = c.Title
// c.Data["username"] = "kataras " + c.Params.Get("userid")
// c.Data["connstring"] = c.DB.Connstring
// c.Data["uptime"] = time.Now().Sub(c.CreatedAt).Seconds()
// }
//
// Usage: app.Controller("/user/{id:int}", &UserController{
// CreatedAt: time.Now(),
// Title: "User page",
// DB: yourDB,
// })
//
// Look `core/router#APIBuilder#Controller` method too.
//
// A shortcut for the `mvc#Controller`,
// useful when `app.Controller` method is being used.
//
// A Controller can be declared by importing
// the "github.com/kataras/iris/mvc"
// package for machines that have not installed go1.9 yet.
Controller = mvc.Controller
// SessionController is a simple `Controller` implementation
// which requires a binded session manager in order to give
// direct access to the current client's session via its `Session` field.
SessionController = mvc.SessionController
// C is the lightweight BaseController type as an alternative of the `Controller` struct type.
// It contains only the Name of the controller and the Context, it's the best option
// to balance the performance cost reflection uses
// if your controller uses the new func output values dispatcher feature;
// func(c *ExampleController) Get() string |
// (string, string) |
// (string, int) |
// int |
// (int, string |
// (string, error) |
// error |
// (int, error) |
// (customStruct, error) |
// customStruct |
// (customStruct, int) |
// (customStruct, string) |
// Result or (Result, error)
// where Get is an HTTP Method func.
//
// Look `core/router#APIBuilder#Controller` method too.
//
// A shortcut for the `mvc#C`,
// useful when `app.Controller` method is being used.
//
// A C controller can be declared by importing
// the "github.com/kataras/iris/mvc" as well.
C = mvc.C
// Response completes the `mvc/activator/methodfunc.Result` interface.
// It's being used as an alternative return value which
// wraps the status code, the content type, a content as bytes or as string
// and an error, it's smart enough to complete the request and send the correct response to the client.
//
// A shortcut for the `mvc#Response`,
// useful when return values from method functions, i.e
// GetHelloworld() iris.Response { iris.Response{ Text:"Hello World!", Code: 200 }}
Response = mvc.Response
// View completes the `mvc/activator/methodfunc.Result` interface.
// It's being used as an alternative return value which
// wraps the template file name, layout, (any) view data, status code and error.
// It's smart enough to complete the request and send the correct response to the client.
//
// A shortcut for the `mvc#View`,
// useful when return values from method functions, i.e
// GetUser() iris.View { iris.View{ Name:"user.html", Data: currentUser } }
View = mvc.View
// Result is a response dispatcher.
// All types that complete this interface
// can be returned as values from the method functions.
// A shortcut for the `mvc#Result` which is a shortcut for `mvc/activator/methodfunc#Result`,
// useful when return values from method functions, i.e
// GetUser() iris.Result { iris.Response{} or a custom iris.Result }
// Can be also used for the TryResult function.
Result = mvc.Result
) )
// Try is a shortcut for the function `mvc.Try` result.
// See more at `mvc#Try` documentation.
var Try = mvc.Try

View File

@ -1,30 +0,0 @@
package activator
// CallOnActivate simply calls the "controller"'s `OnActivate(*TController)` function,
// if any.
//
// Look `activator.go#Register` and `ActivateListener` for more.
func CallOnActivate(controller interface{}, tController *TController) {
if ac, ok := controller.(ActivateListener); ok {
ac.OnActivate(tController)
}
}
// ActivateListener is an interface which should be declared
// on a Controller which needs to register or change the bind values
// that the caller-"user" has been passed to; via the `app.Controller`.
// If that interface is completed by a controller
// then the `OnActivate` function will be called ONCE, NOT in every request
// but ONCE at the application's lifecycle.
type ActivateListener interface {
// OnActivate accepts a pointer to the `TController`.
//
// The `Controller` can make use of the `OnActivate` function
// to register custom routes
// or modify the provided values that will be binded to the
// controller later on.
//
// Look `TController` for more.
OnActivate(*TController)
}

View File

@ -1,346 +0,0 @@
package activator
import (
"reflect"
"strings"
"github.com/kataras/iris/core/router/macro"
"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"
"github.com/kataras/iris/context"
"github.com/kataras/iris/core/errors"
)
type (
// TController is the type of the controller,
// it contains all the necessary information to load
// and serve the controller to the outside world,
// 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
valuePtr reflect.Value
// // Methods and handlers, available after the Activate, can be seted `OnActivate` event as well.
// Methods []methodfunc.MethodFunc
Router RegisterFunc
binder *binder // executed even before the BeginRequest if not nil.
modelController *model.Controller
persistenceController *persistence.Controller
}
)
// 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
// to compare the type of the base controller field
// with this "ctrl", see `buildTypeInfo` and `buildMethodHandler`.
var (
// ErrMissingControllerInstance is a static error which fired from `Controller` when
// the passed "c" instnace is not a valid type of `Controller` or `C`.
ErrMissingControllerInstance = errors.New("controller should have a field of mvc.Controller or mvc.C type")
// ErrInvalidControllerType fired when the "Controller" field is not
// the correct type.
ErrInvalidControllerType = errors.New("controller instance is not a valid implementation")
)
// BaseController is the controller interface,
// which the main request `Controller` will implement automatically.
// End-User doesn't need to have any knowledge of this if she/he doesn't want to implement
// a new Controller type.
// Controller looks the whole flow as one handler, so `ctx.Next`
// inside `BeginRequest` is not be respected.
// Alternative way to check if a middleware was procceed successfully
// and called its `ctx.Next` is the `ctx.Proceed(handler) bool`.
// You have to navigate to the `context/context#Proceed` function's documentation.
type BaseController interface {
SetName(name string)
BeginRequest(ctx context.Context)
EndRequest(ctx context.Context)
}
// ActivateController returns a new controller type info description.
func newController(base BaseController, router RegisterFunc) (*TController, error) {
// get and save the type.
typ := reflect.TypeOf(base)
if typ.Kind() != reflect.Ptr {
typ = reflect.PtrTo(typ)
}
valPointer := reflect.ValueOf(base) // or value raw
// first instance value, needed to validate
// the actual type of the controller field
// and to collect and save the instance's persistence fields'
// values later on.
val := reflect.Indirect(valPointer)
ctrlName := val.Type().Name()
pkgPath := val.Type().PkgPath()
fullName := pkgPath[strings.LastIndexByte(pkgPath, '/')+1:] + "." + ctrlName
t := &TController{
Name: ctrlName,
FullName: fullName,
Type: typ,
Value: val,
valuePtr: valPointer,
Router: router,
binder: &binder{elemType: typ.Elem()},
modelController: model.Load(typ),
persistenceController: persistence.Load(typ, val),
}
return t, nil
}
// BindValueTypeExists returns true if at least one type of "bindValue"
// is already binded to this `TController`.
func (t *TController) BindValueTypeExists(bindValue interface{}) bool {
valueTyp := reflect.TypeOf(bindValue)
for _, bindedValue := range t.binder.values {
// type already exists, remember: binding here is per-type.
if typ := reflect.TypeOf(bindedValue); typ == valueTyp ||
(valueTyp.Kind() == reflect.Interface && typ.Implements(valueTyp)) {
return true
}
}
return false
}
// BindValue binds a value to a controller's field when request is served.
func (t *TController) BindValue(bindValues ...interface{}) {
for _, bindValue := range bindValues {
t.binder.bind(bindValue)
}
}
// 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
hasBinder = !t.binder.isEmpty()
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).
c := reflect.New(elem)
if hasBinder {
t.binder.handle(c)
}
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)
}
// if previous (binded) handlers stopped the execution
// we should know that.
if ctx.IsStopped() {
return
}
// init the request.
b.BeginRequest(ctx)
if ctx.IsStopped() { // if begin request stopped the execution
return
}
// 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))
// if had models, set them after the end-developer's handler.
if hasModels {
t.modelController.Handle(ctx, c)
}
// end the request, don't check for stopped because this does the actual writing
// if no response written already.
b.EndRequest(ctx)
}
}
func (t *TController) registerMethodFunc(m methodfunc.MethodFunc) {
var middleware context.Handlers
if !t.binder.isEmpty() {
if m := t.binder.middleware; len(m) > 0 {
middleware = m
}
}
h := t.HandlerOf(m)
if h == nil {
golog.Warnf("MVC %s: nil method handler found for %s", t.FullName, m.Name)
return
}
registeredHandlers := append(middleware, h)
t.Router(m.HTTPMethod, m.RelPath, registeredHandlers...)
golog.Debugf("MVC %s: %s %s maps to function[%d] '%s'", t.FullName,
m.HTTPMethod,
m.RelPath,
m.Index,
m.Name)
}
func (t *TController) resolveAndRegisterMethods() {
// the actual method functions
// i.e for "GET" it's the `Get()`.
methods, err := methodfunc.Resolve(t.Type)
if err != nil {
golog.Errorf("MVC %s: %s", t.FullName, err.Error())
return
}
// 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.
for _, m := range methods {
t.registerMethodFunc(m)
}
}
// Handle registers a method func but with a custom http method and relative route's path,
// it respects the rest of the controller's rules and guidelines.
func (t *TController) Handle(httpMethod, path, handlerFuncName string) bool {
cTyp := t.Type // with the pointer.
m, exists := cTyp.MethodByName(handlerFuncName)
if !exists {
golog.Errorf("MVC: function '%s' doesn't exist inside the '%s' controller",
handlerFuncName, t.FullName)
return false
}
info := methodfunc.FuncInfo{
Name: m.Name,
Trailing: m.Name,
Type: m.Type,
Index: m.Index,
HTTPMethod: httpMethod,
}
tmpl, err := macro.Parse(path, macro.NewMap())
if err != nil {
golog.Errorf("MVC: fail to parse the path for '%s.%s': %v", t.FullName, handlerFuncName, err)
return false
}
paramKeys := make([]string, len(tmpl.Params), len(tmpl.Params))
for i, param := range tmpl.Params {
paramKeys[i] = param.Name
}
methodFunc, err := methodfunc.ResolveMethodFunc(info, paramKeys...)
if err != nil {
golog.Errorf("MVC: function '%s' inside the '%s' controller: %v", handlerFuncName, t.FullName, err)
return false
}
methodFunc.RelPath = path
t.registerMethodFunc(methodFunc)
return true
}
// func (t *TController) getMethodFuncByName(funcName string) (methodfunc.MethodFunc, bool) {
// cVal := t.Value
// cTyp := t.Type // with the pointer.
// m, exists := cTyp.MethodByName(funcName)
// if !exists {
// golog.Errorf("MVC: function '%s' doesn't exist inside the '%s' controller",
// funcName, cTyp.String())
// return methodfunc.MethodFunc{}, false
// }
// fn := cVal.MethodByName(funcName)
// if !fn.IsValid() {
// golog.Errorf("MVC: function '%s' inside the '%s' controller has not a valid value",
// funcName, cTyp.String())
// return methodfunc.MethodFunc{}, false
// }
// info, ok := methodfunc.FetchFuncInfo(m)
// if !ok {
// golog.Errorf("MVC: could not resolve the func info from '%s'", funcName)
// return methodfunc.MethodFunc{}, false
// }
// methodFunc, err := methodfunc.ResolveMethodFunc(info)
// if err != nil {
// golog.Errorf("MVC: %v", err)
// return methodfunc.MethodFunc{}, false
// }
// return methodFunc, true
// }
// // RegisterName registers a function by its name
// func (t *TController) RegisterName(funcName string) bool {
// methodFunc, ok := t.getMethodFuncByName(funcName)
// if !ok {
// return false
// }
// t.registerMethodFunc(methodFunc)
// return true
// }
// RegisterFunc used by the caller to register the result routes.
type RegisterFunc func(httpMethod string, relPath string, handler ...context.Handler)
// Register receives a "controller",
// a pointer of an instance which embeds the `Controller`,
// the value of "baseControllerFieldName" should be `Controller`.
func Register(controller BaseController, bindValues []interface{},
registerFunc RegisterFunc) error {
t, err := newController(controller, registerFunc)
if err != nil {
return err
}
t.BindValue(bindValues...)
CallOnActivate(controller, t)
for _, bf := range t.binder.fields {
golog.Debugf("MVC %s: binder loaded for '%s' with value:\n%#v",
t.FullName, bf.GetFullName(), bf.GetValue())
}
t.resolveAndRegisterMethods()
return nil
}

View File

@ -1,108 +0,0 @@
package activator
import (
"reflect"
"github.com/kataras/iris/mvc/activator/field"
"github.com/kataras/iris/context"
)
// binder accepts a value of something
// and tries to find its equalivent type
// inside the controller and sets that to it,
// after that each new instance of the controller will have
// this value on the specific field, like persistence data control does.
type binder struct {
elemType reflect.Type
// values and fields are matched on the `match`.
values []interface{}
fields []field.Field
// saves any middleware that may need to be passed to the router,
// statically, to gain performance.
middleware context.Handlers
}
func (b *binder) bind(value interface{}) {
if value == nil {
return
}
b.values = append(b.values, value) // keep values.
b.match(value)
}
func (b *binder) isEmpty() bool {
// if nothing valid found return nil, so the caller
// can omit the binder.
if len(b.fields) == 0 && len(b.middleware) == 0 {
return true
}
return false
}
func (b *binder) storeValueIfMiddleware(value reflect.Value) bool {
if value.CanInterface() {
if m, ok := value.Interface().(context.Handler); ok {
b.middleware = append(b.middleware, m)
return true
}
if m, ok := value.Interface().(func(context.Context)); ok {
b.middleware = append(b.middleware, m)
return true
}
}
return false
}
func (b *binder) match(v interface{}) {
value := reflect.ValueOf(v)
// handlers will be recognised as middleware, not struct fields.
// End-Developer has the option to call any handler inside
// the controller's `BeginRequest` and `EndRequest`, the
// state is respected from the method handler already.
if b.storeValueIfMiddleware(value) {
// stored as middleware, continue to the next field, we don't have
// to bind anything here.
return
}
matcher := func(elemField reflect.StructField) bool {
// If the controller's field is interface then check
// if the given binded value implements that interface.
// i.e MovieController { Service services.MovieService /* interface */ }
// app.Controller("/", new(MovieController),
// services.NewMovieMemoryService(...))
//
// `services.NewMovieMemoryService` returns a `*MovieMemoryService`
// that implements the `MovieService` interface.
if elemField.Type.Kind() == reflect.Interface {
return value.Type().Implements(elemField.Type)
}
return elemField.Type == value.Type()
}
handler := func(f *field.Field) {
f.Value = value
}
b.fields = append(b.fields, field.LookupFields(b.elemType, matcher, handler)...)
}
func (b *binder) handle(c reflect.Value) {
// we could make check for middlewares here but
// these could easly be used outside of the controller
// so we don't have to initialize a controller to call them
// so they don't belong actually here, we will register them to the
// router itself, before the controller's handler to gain performance,
// look `activator.go#RegisterMethodHandlers` for more.
elem := c.Elem() // controller should always be a pointer at this state
for _, f := range b.fields {
f.SendTo(elem)
}
}

View File

@ -1,220 +0,0 @@
package field
import (
"reflect"
)
// 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.
TagName string
Index int
Type reflect.Type
Value reflect.Value
embedded *Field
}
// 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 {
deepIndex := []int{ff.Index}
if emb := ff.embedded; emb != nil {
deepIndex = append(deepIndex, emb.GetIndex()...)
}
return deepIndex
}
// 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 typ
}
// GetFullName returns the full name of that field
// i.e: UserController.SessionController.Manager,
// it's useful for debugging only.
func (ff Field) GetFullName() string {
name := ff.Name
if emb := ff.embedded; emb != nil {
return name + "." + emb.GetFullName()
}
return name
}
// GetTagName returns the tag name of the referring field
// recursively.
func (ff Field) GetTagName() string {
name := ff.TagName
if emb := ff.embedded; emb != nil {
return emb.GetTagName()
}
return name
}
// checkVal checks if that value
// is valid to be set-ed to the new controller's instance.
// Used on binder.
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{} {
if ff.embedded != nil {
return ff.embedded.GetValue()
}
if checkVal(ff.Value) {
return ff.Value.Interface()
}
return "undefinied value"
}
// 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) {
// 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))
}
return
}
elemField := elem.Field(ff.Index)
if elemField.Kind() == reflect.Ptr && !elemField.IsNil() {
return
}
elemField.Set(ff.Value)
}
// lookupTagName checks if the "elemField" struct's field
// contains a tag `name`, if it contains then it returns its value
// otherwise returns the field's original Name.
func lookupTagName(elemField reflect.StructField) string {
vname := elemField.Name
if taggedName, ok := elemField.Tag.Lookup("name"); ok {
vname = taggedName
}
return vname
}
// 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) {
for i, n := 0, elem.NumField(); i < n; i++ {
elemField := elem.Field(i)
if matcher(elemField) {
field := Field{
Index: i,
Name: elemField.Name,
TagName: lookupTagName(elemField),
Type: elemField.Type,
}
if handler != nil {
handler(&field)
}
// we area inside the correct type
fields = append(fields, field)
continue
}
f := lookupStructField(elemField.Type, matcher, handler)
if f != nil {
fields = append(fields, Field{
Index: i,
Name: elemField.Name,
Type: elemField.Type,
embedded: f,
})
}
}
return
}
// lookupStructField is here to search for embedded field only,
// is working with the "lookupFields".
// We could just one one function
// 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 {
// fmt.Printf("lookup struct for elem: %s\n", elem.Name())
// ignore if that field is not a struct
if elem.Kind() != reflect.Struct {
return nil
}
// search by fields.
for i, n := 0, elem.NumField(); i < n; i++ {
elemField := elem.Field(i)
if matcher(elemField) {
// we area inside the correct type.
f := &Field{
Index: i,
Name: elemField.Name,
TagName: lookupTagName(elemField),
Type: elemField.Type,
}
if handler != nil {
handler(f)
}
return f
}
// if field is struct and the value is struct
// then try inside its fields for a compatible
// field type.
if elemField.Type.Kind() == reflect.Struct { // 3-level
elemFieldEmb := elem.Field(i)
f := lookupStructField(elemFieldEmb.Type, matcher, handler)
if f != nil {
fp := &Field{
Index: i,
Name: elemFieldEmb.Name,
TagName: lookupTagName(elemFieldEmb),
Type: elemFieldEmb.Type,
embedded: f,
}
return fp
}
}
}
return nil
}

View File

@ -1,19 +0,0 @@
package methodfunc
import (
"reflect"
"github.com/kataras/iris/context"
)
// buildMethodCall builds the method caller.
// We have repeated code here but it's the only way
// to support more than one input arguments without performance cost compared to previous implementation.
// so it's hard-coded written to check the length of input args and their types.
func buildMethodCall(a *ast) func(ctx context.Context, f reflect.Value) {
// if func input arguments are more than one then
// use the Call method (slower).
return func(ctx context.Context, f reflect.Value) {
DispatchFuncResult(ctx, f.Call(a.paramValues(ctx)))
}
}

View File

@ -1,102 +0,0 @@
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)
if method, ok := FetchFuncInfo(m); ok {
methods = append(methods, method)
}
}
return
}
// FetchFuncInfo returns a FuncInfo based on the method of the controller.
func FetchFuncInfo(m reflect.Method) (FuncInfo, bool) {
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
}
}
info := FuncInfo{
Name: name,
Trailing: trailing,
Type: m.Type,
HTTPMethod: method,
Index: m.Index,
}
return info, true
}
}
return FuncInfo{}, false
}
func methodTitle(httpMethod string) string {
httpMethodFuncName := strings.Title(strings.ToLower(httpMethod))
return httpMethodFuncName
}

View File

@ -1,89 +0,0 @@
package methodfunc
import (
"unicode"
)
const (
tokenBy = "By"
tokenWildcard = "Wildcard" // should be followed by "By",
)
// word lexer, not characters.
type lexer struct {
words []string
cur int
}
func newLexer(s string) *lexer {
l := new(lexer)
l.reset(s)
return l
}
func (l *lexer) reset(trailing string) {
l.cur = -1
var words []string
if trailing != "" {
end := len(trailing)
start := -1
for i, n := 0, end; i < n; i++ {
c := rune(trailing[i])
if unicode.IsUpper(c) {
// it doesn't count the last uppercase
if start != -1 {
end = i
words = append(words, trailing[start:end])
}
start = i
continue
}
end = i + 1
}
if end > 0 && len(trailing) >= end {
words = append(words, trailing[start:end])
}
}
l.words = words
}
func (l *lexer) next() (w string) {
cur := l.cur + 1
if w = l.peek(cur); w != "" {
l.cur++
}
return
}
func (l *lexer) skip() {
if cur := l.cur + 1; cur < len(l.words) {
l.cur = cur
} else {
l.cur = len(l.words) - 1
}
}
func (l *lexer) peek(idx int) string {
if idx < len(l.words) {
return l.words[idx]
}
return ""
}
func (l *lexer) peekNext() (w string) {
return l.peek(l.cur + 1)
}
func (l *lexer) peekPrev() (w string) {
if l.cur > 0 {
cur := l.cur - 1
w = l.words[cur]
}
return w
}

View File

@ -1,213 +0,0 @@
package methodfunc
import (
"errors"
"fmt"
"reflect"
"strings"
"github.com/kataras/iris/context"
)
var posWords = map[int]string{
0: "",
1: "first",
2: "second",
3: "third",
4: "forth",
5: "five",
6: "sixth",
7: "seventh",
8: "eighth",
9: "ninth",
}
func genParamKey(argIdx int) string {
return "param" + posWords[argIdx] // paramfirst, paramsecond...
}
const (
paramTypeInt = "int"
paramTypeLong = "long"
paramTypeBoolean = "boolean"
paramTypeString = "string"
paramTypePath = "path"
)
var macroTypes = map[string]string{
"int": paramTypeInt,
"int64": paramTypeLong,
"bool": paramTypeBoolean,
"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.
}
type funcParser struct {
info FuncInfo
lexer *lexer
}
func newFuncParser(info FuncInfo) *funcParser {
return &funcParser{
info: info,
lexer: newLexer(info.Trailing),
}
}
func (p *funcParser) parse() (*ast, error) {
a := new(ast)
funcArgPos := 0
for {
w := p.lexer.next()
if w == "" {
break
}
if w == tokenBy {
funcArgPos++ // starting with 1 because in typ.NumIn() the first is the struct receiver.
// No need for these:
// ByBy will act like /{param:type}/{param:type} as users expected
// if func input arguments are there, else act By like normal path /by.
//
// if p.lexer.peekPrev() == tokenBy || typ.NumIn() == 1 { // ByBy, then act this second By like a path
// a.relPath += "/" + strings.ToLower(w)
// continue
// }
if err := p.parsePathParam(a, w, funcArgPos); err != nil {
return nil, err
}
continue
}
a.relPath += "/" + strings.ToLower(w)
}
// This fixes a problem when developer misses to append the keyword `By`
// to the method function when input arguments are declared (for path parameters binding).
// We could just use it with `By` keyword but this is not a good practise
// because what happens if we will become naive and declare something like
// Get(id int) and GetBy(username string) or GetBy(id int) ? it's not working because of duplication of the path.
// Docs are clear about that but we are humans, they may do a mistake by accident but
// framework will not allow that.
// So the best thing we can do to help prevent those errors is by printing that message
// below to the developer.
// Note: it should be at the end of the words loop because a.dynamic may be true later on.
if numIn := p.info.Type.NumIn(); numIn > 1 && !a.dynamic {
return nil, fmt.Errorf("found %d input arguments but keyword 'By' is missing from '%s'",
// -1 because end-developer wants to know the actual input arguments, without the struct holder.
numIn-1, p.info.Name)
}
return a, nil
}
func (p *funcParser) parsePathParam(a *ast, w string, funcArgPos int) error {
typ := p.info.Type
if typ.NumIn() <= funcArgPos {
// old:
// return nil, errors.New("keyword 'By' found but length of input receivers are not match for " +
// p.info.Name)
// By found but input arguments are not there, so act like /by path without restricts.
a.relPath += "/" + strings.ToLower(w)
return nil
}
var (
paramKey = genParamKey(funcArgPos) // paramfirst, paramsecond...
paramType = paramTypeString // default string
)
// string, int...
goType := typ.In(funcArgPos).Name()
nextWord := p.lexer.peekNext()
if nextWord == tokenWildcard {
p.lexer.skip() // skip the Wildcard word.
paramType = paramTypePath
} else if pType, ok := macroTypes[goType]; ok {
// it's not wildcard, so check base on our available macro types.
paramType = pType
} else {
return errors.New("invalid syntax for " + p.info.Name)
}
a.paramKeys = append(a.paramKeys, paramKey)
a.paramTypes = append(a.paramTypes, paramType)
// /{paramfirst:path}, /{paramfirst:long}...
a.relPath += fmt.Sprintf("/{%s:%s}", paramKey, paramType)
a.dynamic = true
if nextWord == "" && typ.NumIn() > funcArgPos+1 {
// By is the latest word but func is expected
// more path parameters values, i.e:
// GetBy(name string, age int)
// The caller (parse) doesn't need to know
// about the incremental funcArgPos because
// it will not need it.
return p.parsePathParam(a, nextWord, funcArgPos+1)
}
return nil
}
type ast struct {
paramKeys []string // paramfirst, paramsecond... [0]
paramTypes []string // string, int, long, path... [0]
relPath string
dynamic bool // when paramKeys (and paramTypes, are equal) > 0
}
// moved to func_caller#buildMethodcall, it's bigger and with repeated code
// than this, below function but it's faster.
// func (a *ast) MethodCall(ctx context.Context, f reflect.Value) {
// if a.dynamic {
// f.Call(a.paramValues(ctx))
// return
// }
//
// f.Interface().(func())()
// }
func (a *ast) paramValues(ctx context.Context) []reflect.Value {
if !a.dynamic {
return nil
}
l := len(a.paramKeys)
values := make([]reflect.Value, l, l)
for i := 0; i < l; i++ {
paramKey := a.paramKeys[i]
paramType := a.paramTypes[i]
values[i] = getParamValueFromType(ctx, paramType, paramKey)
}
return values
}
func getParamValueFromType(ctx context.Context, paramType string, paramKey string) reflect.Value {
if paramType == paramTypeInt {
v, _ := ctx.Params().GetInt(paramKey)
return reflect.ValueOf(v)
}
if paramType == paramTypeLong {
v, _ := ctx.Params().GetInt64(paramKey)
return reflect.ValueOf(v)
}
if paramType == paramTypeBoolean {
v, _ := ctx.Params().GetBool(paramKey)
return reflect.ValueOf(v)
}
// string, path...
return reflect.ValueOf(ctx.Params().Get(paramKey))
}

View File

@ -1,230 +0,0 @@
package methodfunc
import (
"reflect"
"strings"
"github.com/kataras/iris/context"
)
// Result is a response dispatcher.
// All types that complete this interface
// can be returned as values from the method functions.
type Result interface {
// Dispatch should sends the response to the context's response writer.
Dispatch(ctx context.Context)
}
const slashB byte = '/'
type compatibleErr interface {
Error() string
}
// DefaultErrStatusCode is the default error status code (400)
// when the response contains an error which is not nil.
var DefaultErrStatusCode = 400
// DispatchErr writes the error to the response.
func DispatchErr(ctx context.Context, status int, err error) {
if status < 400 {
status = DefaultErrStatusCode
}
ctx.StatusCode(status)
if text := err.Error(); text != "" {
ctx.WriteString(text)
ctx.StopExecution()
}
}
// DispatchCommon is being used internally to send
// commonly used data to the response writer with a smart way.
func DispatchCommon(ctx context.Context,
statusCode int, contentType string, content []byte, v interface{}, err error, found bool) {
// if we have a false boolean as a return value
// then skip everything and fire a not found,
// we even don't care about the given status code or the object or the content.
if !found {
ctx.NotFound()
return
}
status := statusCode
if status == 0 {
status = 200
}
if err != nil {
DispatchErr(ctx, status, err)
return
}
// write the status code, the rest will need that before any write ofc.
ctx.StatusCode(status)
if contentType == "" {
// to respect any ctx.ContentType(...) call
// especially if v is not nil.
contentType = ctx.GetContentType()
}
if v != nil {
if d, ok := v.(Result); ok {
// write the content type now (internal check for empty value)
ctx.ContentType(contentType)
d.Dispatch(ctx)
return
}
if strings.HasPrefix(contentType, context.ContentJavascriptHeaderValue) {
_, err = ctx.JSONP(v)
} else if strings.HasPrefix(contentType, context.ContentXMLHeaderValue) {
_, err = ctx.XML(v, context.XML{Indent: " "})
} else {
// defaults to json if content type is missing or its application/json.
_, err = ctx.JSON(v, context.JSON{Indent: " "})
}
if err != nil {
DispatchErr(ctx, status, err)
}
return
}
ctx.ContentType(contentType)
// .Write even len(content) == 0 , this should be called in order to call the internal tryWriteHeader,
// it will not cost anything.
ctx.Write(content)
}
// DispatchFuncResult is being used internally to resolve
// and send the method function's output values to the
// context's response writer using a smart way which
// respects status code, content type, content, custom struct
// and an error type.
// Supports for:
// func(c *ExampleController) Get() string |
// (string, string) |
// (string, int) |
// ...
// int |
// (int, string |
// (string, error) |
// ...
// error |
// (int, error) |
// (customStruct, error) |
// ...
// bool |
// (int, bool) |
// (string, bool) |
// (customStruct, bool) |
// ...
// customStruct |
// (customStruct, int) |
// (customStruct, string) |
// Result or (Result, error) and so on...
//
// where Get is an HTTP METHOD.
func DispatchFuncResult(ctx context.Context, values []reflect.Value) {
numOut := len(values)
if numOut == 0 {
return
}
var (
// if statusCode > 0 then send this status code.
// Except when err != nil then check if status code is < 400 and
// if it's set it as DefaultErrStatusCode.
// Except when found == false, then the status code is 404.
statusCode int
// if not empty then use that as content type,
// if empty and custom != nil then set it to application/json.
contentType string
// if len > 0 then write that to the response writer as raw bytes,
// except when found == false or err != nil or custom != nil.
content []byte
// if not nil then check
// for content type (or json default) and send the custom data object
// except when found == false or err != nil.
custom interface{}
// if not nil then check for its status code,
// if not status code or < 400 then set it as DefaultErrStatusCode
// and fire the error's text.
err error
// if false then skip everything and fire 404.
found = true // defaults to true of course, otherwise will break :)
)
for _, v := range values {
// order of these checks matters
// for example, first we need to check for status code,
// secondly the string (for content type and content)...
if !v.IsValid() {
continue
}
f := v.Interface()
if b, ok := f.(bool); ok {
found = b
if !found {
// skip everything, we don't care about other return values,
// this boolean is the higher in order.
break
}
continue
}
if i, ok := f.(int); ok {
statusCode = i
continue
}
if s, ok := f.(string); ok {
// a string is content type when it contains a slash and
// content or custom struct is being calculated already;
// (string -> content, string-> content type)
// (customStruct, string -> content type)
if (len(content) > 0 || custom != nil) && strings.IndexByte(s, slashB) > 0 {
contentType = s
} else {
// otherwise is content
content = []byte(s)
}
continue
}
if b, ok := f.([]byte); ok {
// it's raw content, get the latest
content = b
continue
}
if e, ok := f.(compatibleErr); ok {
if e != nil { // it's always not nil but keep it here.
err = e
if statusCode < 400 {
statusCode = DefaultErrStatusCode
}
break // break on first error, error should be in the end but we
// need to know break the dispatcher if any error.
// at the end; we don't want to write anything to the response if error is not nil.
}
continue
}
// else it's a custom struct or a dispatcher, we'll decide later
// because content type and status code matters
// do that check in order to be able to correctly dispatch:
// (customStruct, error) -> customStruct filled and error is nil
if custom == nil && f != nil {
custom = f
}
}
DispatchCommon(ctx, statusCode, contentType, content, custom, err, found)
}

View File

@ -1,68 +0,0 @@
package methodfunc
import (
"reflect"
"github.com/kataras/iris/context"
"github.com/kataras/iris/core/errors"
)
// MethodFunc the handler function.
type MethodFunc struct {
FuncInfo
// 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 func(ctx context.Context, f reflect.Value)
RelPath string
}
// Resolve returns all the method funcs
// necessary information and actions to
// perform the request.
func Resolve(typ reflect.Type) ([]MethodFunc, error) {
r := errors.NewReporter()
var methodFuncs []MethodFunc
infos := fetchInfos(typ)
for _, info := range infos {
methodFunc, err := ResolveMethodFunc(info)
if r.AddErr(err) {
continue
}
methodFuncs = append(methodFuncs, methodFunc)
}
return methodFuncs, r.Return()
}
// ResolveMethodFunc resolves a single `MethodFunc` from a single `FuncInfo`.
func ResolveMethodFunc(info FuncInfo, paramKeys ...string) (MethodFunc, error) {
parser := newFuncParser(info)
a, err := parser.parse()
if err != nil {
return MethodFunc{}, err
}
if len(paramKeys) > 0 {
a.paramKeys = paramKeys
}
methodFunc := MethodFunc{
RelPath: a.relPath,
FuncInfo: info,
MethodCall: buildMethodCall(a),
}
/* TODO: split the method path and ast param keys, and all that
because now we want to use custom param keys but 'paramfirst' is set-ed.
*/
return methodFunc, nil
}

View File

@ -1,73 +0,0 @@
package model
import (
"reflect"
"github.com/kataras/iris/mvc/activator/field"
"github.com/kataras/iris/context"
)
// 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
}
// 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" {
return true
}
}
return false
}
fields := field.LookupFields(typ.Elem(), matcher, nil)
if len(fields) == 0 {
return nil
}
mc := &Controller{
fields: fields,
}
return mc
}
// 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()
elemField := elem.FieldByIndex(index)
// check if current controller's element field
// is valid, is not nil and it's type is the same (should be but make that check to be sure).
if !elemField.IsValid() ||
(elemField.Kind() == reflect.Ptr && elemField.IsNil()) ||
elemField.Type() != typ {
continue
}
fieldValue := elemField.Interface()
ctx.ViewData(name, fieldValue)
// /*maybe some time in the future*/ if resetable {
// // clean up
// elemField.Set(reflect.Zero(typ))
// }
}
}

View File

@ -1,60 +0,0 @@
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,369 +0,0 @@
package mvc
import (
"reflect"
"strings"
"github.com/kataras/iris/context"
"github.com/kataras/iris/core/memstore"
"github.com/kataras/iris/mvc/activator"
)
// C is the lightweight BaseController type as an alternative of the `Controller` struct type.
// It contains only the Name of the controller and the Context, it's the best option
// to balance the performance cost reflection uses
// if your controller uses the new func output values dispatcher feature;
// func(c *ExampleController) Get() string |
// (string, string) |
// (string, int) |
// int |
// (int, string |
// (string, error) |
// bool |
// (any, bool) |
// error |
// (int, error) |
// (customStruct, error) |
// customStruct |
// (customStruct, int) |
// (customStruct, string) |
// Result or (Result, error)
// where Get is an HTTP Method func.
//
// Look `core/router#APIBuilder#Controller` method too.
//
// It completes the `activator.BaseController` interface.
//
// Example at: https://github.com/kataras/iris/tree/master/_examples/mvc/overview/web/controllers.
// Example usage at: https://github.com/kataras/iris/blob/master/mvc/method_result_test.go#L17.
type C struct {
// The Name of the `C` controller.
Name string
// The current context.Context.
//
// we have to name it for two reasons:
// 1: can't ignore these via reflection, it doesn't give an option to
// see if the functions is derived from another type.
// 2: end-developer may want to use some method functions
// or any fields that could be conflict with the context's.
Ctx context.Context
}
var _ activator.BaseController = &C{}
// SetName sets the controller's full name.
// It's called internally.
func (c *C) SetName(name string) { c.Name = name }
// BeginRequest starts the request by initializing the `Context` field.
func (c *C) BeginRequest(ctx context.Context) { c.Ctx = ctx }
// EndRequest does nothing, is here to complete the `BaseController` interface.
func (c *C) EndRequest(ctx context.Context) {}
// Controller is the base controller for the high level controllers instances.
//
// This base controller is used as an alternative way of building
// APIs, the controller can register all type of http methods.
//
// Keep note that controllers are bit slow
// because of the reflection use however it's as fast as possible because
// it does preparation before the serve-time handler but still
// remains slower than the low-level handlers
// such as `Handle, Get, Post, Put, Delete, Connect, Head, Trace, Patch`.
//
//
// All fields that are tagged with iris:"persistence"` or binded
// are being persistence and kept the same between the different requests.
//
// An Example Controller can be:
//
// type IndexController struct {
// Controller
// }
//
// func (c *IndexController) Get() {
// c.Tmpl = "index.html"
// c.Data["title"] = "Index page"
// c.Data["message"] = "Hello world!"
// }
//
// Usage: app.Controller("/", new(IndexController))
//
//
// Another example with bind:
//
// type UserController struct {
// mvc.Controller
//
// DB *DB
// CreatedAt time.Time
// }
//
// // Get serves using the User controller when HTTP Method is "GET".
// func (c *UserController) Get() {
// c.Tmpl = "user/index.html"
// c.Data["title"] = "User Page"
// c.Data["username"] = "kataras " + c.Params.Get("userid")
// c.Data["connstring"] = c.DB.Connstring
// c.Data["uptime"] = time.Now().Sub(c.CreatedAt).Seconds()
// }
//
// Usage: app.Controller("/user/{id:int}", new(UserController), db, time.Now())
// Note: Binded values of context.Handler type are being recognised as middlewares by the router.
//
// Look `core/router/APIBuilder#Controller` method too.
//
// It completes the `activator.BaseController` interface.
type Controller struct {
// Name contains the current controller's full name.
//
// doesn't change on different paths.
Name string
// contains the `Name` as different words, all lowercase,
// without the "Controller" suffix if exists.
// we need this as field because the activator
// we will not try to parse these if not needed
// it's up to the end-developer to call `RelPath()` or `RelTmpl()`
// which will result to fill them.
//
// doesn't change on different paths.
nameAsWords []string
// relPath the "as assume" relative request path.
//
// If UserController and request path is "/user/messages" then it's "/messages"
// if UserPostController and request path is "/user/post" then it's "/"
// if UserProfile and request path is "/user/profile/likes" then it's "/likes"
//
// doesn't change on different paths.
relPath string
// request path and its parameters, read-write.
// Path is the current request path, if changed then it redirects.
Path string
// Params are the request path's parameters, i.e
// for route like "/user/{id}" and request to "/user/42"
// it contains the "id" = 42.
Params *context.RequestParams
// some info read and write,
// can be already set-ed by previous handlers as well.
Status int
Values *memstore.Store
// relTmpl the "as assume" relative path to the view root folder.
//
// If UserController then it's "user/"
// if UserPostController then it's "user/post/"
// if UserProfile then it's "user/profile/".
//
// doesn't change on different paths.
relTmpl string
// view read and write,
// can be already set-ed by previous handlers as well.
Layout string
Tmpl string
Data map[string]interface{}
ContentType string
Text string // response as string
// give access to the request context itself.
Ctx context.Context
}
var _ activator.BaseController = &Controller{}
var ctrlSuffix = reflect.TypeOf(Controller{}).Name()
// SetName sets the controller's full name.
// It's called internally.
func (c *Controller) SetName(name string) {
c.Name = name
}
func (c *Controller) getNameWords() []string {
if len(c.nameAsWords) == 0 {
c.nameAsWords = findCtrlWords(c.Name)
}
return c.nameAsWords
}
// Route returns the current request controller's context read-only access route.
func (c *Controller) Route() context.RouteReadOnly {
return c.Ctx.GetCurrentRoute()
}
const slashStr = "/"
// RelPath tries to return the controller's name
// without the "Controller" prefix, all lowercase
// prefixed with slash and splited by slash appended
// with the rest of the request path.
// For example:
// If UserController and request path is "/user/messages" then it's "/messages"
// if UserPostController and request path is "/user/post" then it's "/"
// if UserProfile and request path is "/user/profile/likes" then it's "/likes"
//
// It's useful for things like path checking and redirect.
func (c *Controller) RelPath() string {
if c.relPath == "" {
w := c.getNameWords()
rel := strings.Join(w, slashStr)
reqPath := c.Ctx.Path()
if len(reqPath) == 0 {
// it never come here
// but to protect ourselves just return an empty slash.
return slashStr
}
// [1:]to ellimuate the prefixes like "//"
// request path has always "/"
rel = strings.Replace(reqPath[1:], rel, "", 1)
if rel == "" {
rel = slashStr
}
c.relPath = rel
// this will return any dynamic path after the static one
// or a a slash "/":
//
// reqPath := c.Ctx.Path()
// if len(reqPath) == 0 {
// // it never come here
// // but to protect ourselves just return an empty slash.
// return slashStr
// }
// var routeVParams []string
// c.Params.Visit(func(key string, value string) {
// routeVParams = append(routeVParams, value)
// })
// rel := c.Route().StaticPath()
// println(rel)
// // [1:]to ellimuate the prefixes like "//"
// // request path has always "/"
// rel = strings.Replace(reqPath, rel[1:], "", 1)
// println(rel)
// if rel == "" {
// rel = slashStr
// }
// c.relPath = rel
}
return c.relPath
}
// RelTmpl tries to return the controller's name
// without the "Controller" prefix, all lowercase
// splited by slash and suffixed by slash.
// For example:
// If UserController then it's "user/"
// if UserPostController then it's "user/post/"
// if UserProfile then it's "user/profile/".
//
// It's useful to locate templates if the controller and views path have aligned names.
func (c *Controller) RelTmpl() string {
if c.relTmpl == "" {
c.relTmpl = strings.Join(c.getNameWords(), slashStr) + slashStr
}
return c.relTmpl
}
// Write writes to the client via the context's ResponseWriter.
// Controller completes the `io.Writer` interface for the shake of ease.
func (c *Controller) Write(contents []byte) (int, error) {
c.tryWriteHeaders()
return c.Ctx.ResponseWriter().Write(contents)
}
// Writef formats according to a format specifier and writes to the response.
func (c *Controller) Writef(format string, a ...interface{}) (int, error) {
c.tryWriteHeaders()
return c.Ctx.ResponseWriter().Writef(format, a...)
}
// BeginRequest starts the main controller
// it initialize the Ctx and other fields.
//
// It's called internally.
// End-Developer can ovverride it but it still MUST be called.
func (c *Controller) BeginRequest(ctx context.Context) {
// path and path params
c.Path = ctx.Path()
c.Params = ctx.Params()
// response status code
c.Status = ctx.GetStatusCode()
// share values
c.Values = ctx.Values()
// view data for templates, remember
// each controller is a new instance, so
// checking for nil and then init those type of fields
// have no meaning.
c.Data = make(map[string]interface{}, 0)
// context itself
c.Ctx = ctx
}
func (c *Controller) tryWriteHeaders() {
if c.Status > 0 && c.Status != c.Ctx.GetStatusCode() {
c.Ctx.StatusCode(c.Status)
}
if c.ContentType != "" {
c.Ctx.ContentType(c.ContentType)
}
}
// EndRequest is the final method which will be executed
// before response sent.
//
// It checks for the fields and calls the necessary context's
// methods to modify the response to the client.
//
// It's called internally.
// End-Developer can ovveride it but still should be called at the end.
func (c *Controller) EndRequest(ctx context.Context) {
if ctx.ResponseWriter().Written() >= 0 { // status code only (0) or actual body written(>0)
return
}
if path := c.Path; path != "" && path != ctx.Path() {
// then redirect and exit.
ctx.Redirect(path, c.Status)
return
}
c.tryWriteHeaders()
if response := c.Text; response != "" {
ctx.WriteString(response)
return // exit here
}
if view := c.Tmpl; view != "" {
if layout := c.Layout; layout != "" {
ctx.ViewLayout(layout)
}
if len(c.Data) > 0 {
dataKey := ctx.Application().ConfigurationReadOnly().GetViewDataContextKey()
// In order to respect any c.Ctx.ViewData that may called manually before;
if ctx.Values().Get(dataKey) == nil {
// if no c.Ctx.ViewData then it's empty do a
// pure set, it's faster.
ctx.Values().Set(dataKey, c.Data)
} else {
// else do a range loop and set the data one by one.
for k, v := range c.Data {
ctx.ViewData(k, v)
}
}
}
ctx.View(view)
}
}

View File

@ -1,531 +0,0 @@
// black-box testing
package mvc_test
import (
"testing"
"github.com/kataras/iris"
"github.com/kataras/iris/context"
"github.com/kataras/iris/mvc"
"github.com/kataras/iris/mvc/activator"
"github.com/kataras/iris/core/router"
"github.com/kataras/iris/httptest"
)
type testController struct {
mvc.Controller
}
var writeMethod = func(c mvc.Controller) {
c.Ctx.Writef(c.Ctx.Method())
}
func (c *testController) Get() {
writeMethod(c.Controller)
}
func (c *testController) Post() {
writeMethod(c.Controller)
}
func (c *testController) Put() {
writeMethod(c.Controller)
}
func (c *testController) Delete() {
writeMethod(c.Controller)
}
func (c *testController) Connect() {
writeMethod(c.Controller)
}
func (c *testController) Head() {
writeMethod(c.Controller)
}
func (c *testController) Patch() {
writeMethod(c.Controller)
}
func (c *testController) Options() {
writeMethod(c.Controller)
}
func (c *testController) Trace() {
writeMethod(c.Controller)
}
type (
testControllerAll struct{ mvc.Controller }
testControllerAny struct{ mvc.Controller } // exactly the same as All
)
func (c *testControllerAll) All() {
writeMethod(c.Controller)
}
func (c *testControllerAny) Any() {
writeMethod(c.Controller)
}
func TestControllerMethodFuncs(t *testing.T) {
app := iris.New()
app.Controller("/", new(testController))
app.Controller("/all", new(testControllerAll))
app.Controller("/any", new(testControllerAny))
e := httptest.New(t, app)
for _, method := range router.AllMethods {
e.Request(method, "/").Expect().Status(iris.StatusOK).
Body().Equal(method)
e.Request(method, "/all").Expect().Status(iris.StatusOK).
Body().Equal(method)
e.Request(method, "/any").Expect().Status(iris.StatusOK).
Body().Equal(method)
}
}
func TestControllerMethodAndPathHandleMany(t *testing.T) {
app := iris.New()
app.Controller("/ /path1 /path2 /path3", new(testController))
e := httptest.New(t, app)
for _, method := range router.AllMethods {
e.Request(method, "/").Expect().Status(iris.StatusOK).
Body().Equal(method)
e.Request(method, "/path1").Expect().Status(iris.StatusOK).
Body().Equal(method)
e.Request(method, "/path2").Expect().Status(iris.StatusOK).
Body().Equal(method)
}
}
type testControllerPersistence struct {
mvc.Controller
Data string `iris:"persistence"`
}
func (c *testControllerPersistence) Get() {
c.Ctx.WriteString(c.Data)
}
func TestControllerPersistenceFields(t *testing.T) {
data := "this remains the same for all requests"
app := iris.New()
app.Controller("/", &testControllerPersistence{Data: data})
e := httptest.New(t, app)
e.GET("/").Expect().Status(iris.StatusOK).
Body().Equal(data)
}
type testControllerBeginAndEndRequestFunc struct {
mvc.Controller
Username string
}
// called before of every method (Get() or Post()).
//
// useful when more than one methods using the
// same request values or context's function calls.
func (c *testControllerBeginAndEndRequestFunc) BeginRequest(ctx context.Context) {
c.Controller.BeginRequest(ctx)
c.Username = ctx.Params().Get("username")
// or t.Params.Get("username") because the
// t.Ctx == ctx and is being initialized at the t.Controller.BeginRequest.
}
// called after every method (Get() or Post()).
func (c *testControllerBeginAndEndRequestFunc) EndRequest(ctx context.Context) {
ctx.Writef("done") // append "done" to the response
c.Controller.EndRequest(ctx)
}
func (c *testControllerBeginAndEndRequestFunc) Get() {
c.Ctx.Writef(c.Username)
}
func (c *testControllerBeginAndEndRequestFunc) Post() {
c.Ctx.Writef(c.Username)
}
func TestControllerBeginAndEndRequestFunc(t *testing.T) {
app := iris.New()
app.Controller("/profile/{username}", new(testControllerBeginAndEndRequestFunc))
e := httptest.New(t, app)
usernames := []string{
"kataras",
"makis",
"efi",
"rg",
"bill",
"whoisyourdaddy",
}
doneResponse := "done"
for _, username := range usernames {
e.GET("/profile/" + username).Expect().Status(iris.StatusOK).
Body().Equal(username + doneResponse)
e.POST("/profile/" + username).Expect().Status(iris.StatusOK).
Body().Equal(username + doneResponse)
}
}
func TestControllerBeginAndEndRequestFuncBindMiddleware(t *testing.T) {
app := iris.New()
usernames := map[string]bool{
"kataras": true,
"makis": false,
"efi": true,
"rg": false,
"bill": true,
"whoisyourdaddy": false,
}
middlewareCheck := func(ctx context.Context) {
for username, allow := range usernames {
if ctx.Params().Get("username") == username && allow {
ctx.Next()
return
}
}
ctx.StatusCode(iris.StatusForbidden)
ctx.Writef("forbidden")
}
app.Controller("/profile/{username}", new(testControllerBeginAndEndRequestFunc), middlewareCheck)
e := httptest.New(t, app)
doneResponse := "done"
for username, allow := range usernames {
getEx := e.GET("/profile/" + username).Expect()
if allow {
getEx.Status(iris.StatusOK).
Body().Equal(username + doneResponse)
} else {
getEx.Status(iris.StatusForbidden).Body().Equal("forbidden")
}
postEx := e.POST("/profile/" + username).Expect()
if allow {
postEx.Status(iris.StatusOK).
Body().Equal(username + doneResponse)
} else {
postEx.Status(iris.StatusForbidden).Body().Equal("forbidden")
}
}
}
type Model struct {
Username string
}
type testControllerModel struct {
mvc.Controller
TestModel Model `iris:"model" name:"myModel"`
TestModel2 Model `iris:"model"`
}
func (c *testControllerModel) Get() {
username := c.Ctx.Params().Get("username")
c.TestModel = Model{Username: username}
c.TestModel2 = Model{Username: username + "2"}
}
func writeModels(ctx context.Context, names ...string) {
if expected, got := len(names), len(ctx.GetViewData()); expected != got {
ctx.Writef("expected view data length: %d but got: %d for names: %s", expected, got, names)
return
}
for _, name := range names {
m, ok := ctx.GetViewData()[name]
if !ok {
ctx.Writef("fail load and set the %s", name)
return
}
model, ok := m.(Model)
if !ok {
ctx.Writef("fail to override the %s' name by the tag", name)
return
}
ctx.Writef(model.Username)
}
}
func (c *testControllerModel) EndRequest(ctx context.Context) {
writeModels(ctx, "myModel", "TestModel2")
c.Controller.EndRequest(ctx)
}
func TestControllerModel(t *testing.T) {
app := iris.New()
app.Controller("/model/{username}", new(testControllerModel))
e := httptest.New(t, app)
usernames := []string{
"kataras",
"makis",
}
for _, username := range usernames {
e.GET("/model/" + username).Expect().Status(iris.StatusOK).
Body().Equal(username + username + "2")
}
}
type testBindType struct {
title string
}
type testControllerBindStruct struct {
mvc.Controller
// should start with upper letter of course
TitlePointer *testBindType // should have the value of the "myTitlePtr" on test
TitleValue testBindType // should have the value of the "myTitleV" on test
Other string // just another type to check the field collection, should be empty
}
func (t *testControllerBindStruct) Get() {
t.Ctx.Writef(t.TitlePointer.title + t.TitleValue.title + t.Other)
}
type testControllerBindDeep struct {
testControllerBindStruct
}
func (t *testControllerBindDeep) Get() {
// t.testControllerBindStruct.Get()
t.Ctx.Writef(t.TitlePointer.title + t.TitleValue.title + t.Other)
}
func TestControllerBind(t *testing.T) {
app := iris.New()
// app.Logger().SetLevel("debug")
t1, t2 := "my pointer title", "val title"
// test bind pointer to pointer of the correct type
myTitlePtr := &testBindType{title: t1}
// test bind value to value of the correct type
myTitleV := testBindType{title: t2}
app.Controller("/", new(testControllerBindStruct), myTitlePtr, myTitleV)
app.Controller("/deep", new(testControllerBindDeep), myTitlePtr, myTitleV)
e := httptest.New(t, app)
expected := t1 + t2
e.GET("/").Expect().Status(iris.StatusOK).
Body().Equal(expected)
e.GET("/deep").Expect().Status(iris.StatusOK).
Body().Equal(expected)
}
type (
UserController struct{ mvc.Controller }
Profile struct{ mvc.Controller }
UserProfilePostController struct{ mvc.Controller }
)
func writeRelatives(c mvc.Controller) {
c.Ctx.JSON(context.Map{
"RelPath": c.RelPath(),
"TmplPath": c.RelTmpl(),
})
}
func (c *UserController) Get() {
writeRelatives(c.Controller)
}
func (c *Profile) Get() {
writeRelatives(c.Controller)
}
func (c *UserProfilePostController) Get() {
writeRelatives(c.Controller)
}
func TestControllerRelPathAndRelTmpl(t *testing.T) {
app := iris.New()
var tests = map[string]context.Map{
// UserController
"/user": {"RelPath": "/", "TmplPath": "user/"},
"/user/42": {"RelPath": "/42", "TmplPath": "user/"},
"/user/me": {"RelPath": "/me", "TmplPath": "user/"},
// Profile (without Controller suffix, should work as expected)
"/profile": {"RelPath": "/", "TmplPath": "profile/"},
"/profile/42": {"RelPath": "/42", "TmplPath": "profile/"},
"/profile/me": {"RelPath": "/me", "TmplPath": "profile/"},
// UserProfilePost
"/user/profile/post": {"RelPath": "/", "TmplPath": "user/profile/post/"},
"/user/profile/post/42": {"RelPath": "/42", "TmplPath": "user/profile/post/"},
"/user/profile/post/mine": {"RelPath": "/mine", "TmplPath": "user/profile/post/"},
}
app.Controller("/user /user/me /user/{id}",
new(UserController))
app.Controller("/profile /profile/me /profile/{id}",
new(Profile))
app.Controller("/user/profile/post /user/profile/post/mine /user/profile/post/{id}",
new(UserProfilePostController))
e := httptest.New(t, app)
for path, tt := range tests {
e.GET(path).Expect().Status(iris.StatusOK).JSON().Equal(tt)
}
}
type testCtrl0 struct {
testCtrl00
}
func (c *testCtrl0) Get() {
username := c.Params.Get("username")
c.Model = Model{Username: username}
}
func (c *testCtrl0) EndRequest(ctx context.Context) {
writeModels(ctx, "myModel")
if c.TitlePointer == nil {
ctx.Writef("\nTitlePointer is nil!\n")
} else {
ctx.Writef(c.TitlePointer.title)
}
//should be the same as `.testCtrl000.testCtrl0000.EndRequest(ctx)`
c.testCtrl00.EndRequest(ctx)
}
type testCtrl00 struct {
testCtrl000
Model Model `iris:"model" name:"myModel"`
}
type testCtrl000 struct {
testCtrl0000
TitlePointer *testBindType
}
type testCtrl0000 struct {
mvc.Controller
}
func (c *testCtrl0000) EndRequest(ctx context.Context) {
ctx.Writef("finish")
}
func TestControllerInsideControllerRecursively(t *testing.T) {
var (
username = "gerasimos"
title = "mytitle"
expected = username + title + "finish"
)
app := iris.New()
app.Controller("/user/{username}", new(testCtrl0),
&testBindType{title: title})
e := httptest.New(t, app)
e.GET("/user/" + username).Expect().
Status(iris.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) GetBy(int64) {}
func (c *testControllerRelPathFromFunc) GetAnythingByWildcard(string) {}
func (c *testControllerRelPathFromFunc) GetLogin() {}
func (c *testControllerRelPathFromFunc) PostLogin() {}
func (c *testControllerRelPathFromFunc) GetAdminLogin() {}
func (c *testControllerRelPathFromFunc) PutSomethingIntoThis() {}
func (c *testControllerRelPathFromFunc) GetSomethingBy(bool) {}
func (c *testControllerRelPathFromFunc) GetSomethingByBy(string, int) {}
func (c *testControllerRelPathFromFunc) GetSomethingNewBy(string, int) {} // two input arguments, one By which is the latest word.
func (c *testControllerRelPathFromFunc) GetSomethingByElseThisBy(bool, int) {} // two input arguments
func TestControllerRelPathFromFunc(t *testing.T) {
app := iris.New()
app.Controller("/", new(testControllerRelPathFromFunc))
e := httptest.New(t, app)
e.GET("/").Expect().Status(iris.StatusOK).
Body().Equal("GET:/")
e.GET("/42").Expect().Status(iris.StatusOK).
Body().Equal("GET:/42")
e.GET("/something/true").Expect().Status(iris.StatusOK).
Body().Equal("GET:/something/true")
e.GET("/something/false").Expect().Status(iris.StatusOK).
Body().Equal("GET:/something/false")
e.GET("/something/truee").Expect().Status(iris.StatusNotFound)
e.GET("/something/falsee").Expect().Status(iris.StatusNotFound)
e.GET("/something/kataras/42").Expect().Status(iris.StatusOK).
Body().Equal("GET:/something/kataras/42")
e.GET("/something/new/kataras/42").Expect().Status(iris.StatusOK).
Body().Equal("GET:/something/new/kataras/42")
e.GET("/something/true/else/this/42").Expect().Status(iris.StatusOK).
Body().Equal("GET:/something/true/else/this/42")
e.GET("/login").Expect().Status(iris.StatusOK).
Body().Equal("GET:/login")
e.POST("/login").Expect().Status(iris.StatusOK).
Body().Equal("POST:/login")
e.GET("/admin/login").Expect().Status(iris.StatusOK).
Body().Equal("GET:/admin/login")
e.PUT("/something/into/this").Expect().Status(iris.StatusOK).
Body().Equal("PUT:/something/into/this")
e.GET("/42").Expect().Status(iris.StatusOK).
Body().Equal("GET:/42")
e.GET("/anything/here").Expect().Status(iris.StatusOK).
Body().Equal("GET:/anything/here")
}
type testControllerActivateListener struct {
mvc.Controller
TitlePointer *testBindType
}
func (c *testControllerActivateListener) OnActivate(t *activator.TController) {
t.BindValue(&testBindType{
title: "default title",
})
}
func (c *testControllerActivateListener) Get() {
c.Text = c.TitlePointer.title
}
func TestControllerActivateListener(t *testing.T) {
app := iris.New()
app.Controller("/", new(testControllerActivateListener))
app.Controller("/manual", new(testControllerActivateListener), &testBindType{
title: "my title",
})
e := httptest.New(t, app)
e.GET("/").Expect().Status(iris.StatusOK).
Body().Equal("default title")
e.GET("/manual").Expect().Status(iris.StatusOK).
Body().Equal("my title")
}

View File

@ -1,26 +0,0 @@
// +build go1.9
package mvc
import (
"html/template"
"github.com/kataras/iris/mvc/activator"
)
type (
// HTML wraps the "s" with the template.HTML
// in order to be marked as safe content, to be rendered as html and not escaped.
HTML = template.HTML
// TController contains the necessary controller's pre-serve information.
//
// With `TController` the `Controller` can register custom routes
// or modify the provided values that will be binded to the
// controller later on.
//
// Look the `mvc/activator#TController` for its implementation.
//
// A shortcut for the `mvc/activator#TController`, useful when `OnActivate` is being used.
TController = activator.TController
)

View File

@ -1,58 +0,0 @@
package mvc
import (
"github.com/kataras/iris/context"
"github.com/kataras/iris/mvc/activator/methodfunc"
)
// build go1.9 only(go19.go)-->
// // Result is a response dispatcher.
// // All types that complete this interface
// // can be returned as values from the method functions.
// Result = methodfunc.Result
// <--
// No, let's just copy-paste in order to go 1.8 users have this type
// easy to be used from the root mvc package,
// sometimes duplication doesn't hurt.
// Result is a response dispatcher.
// All types that complete this interface
// can be returned as values from the method functions.
//
// Example at: https://github.com/kataras/iris/tree/master/_examples/mvc/overview.
type Result interface { // NOTE: Should be always compatible with the methodfunc.Result.
// Dispatch should sends the response to the context's response writer.
Dispatch(ctx context.Context)
}
var defaultFailureResponse = Response{Code: methodfunc.DefaultErrStatusCode}
// Try will check if "fn" ran without any panics,
// using recovery,
// and return its result as the final response
// otherwise it returns the "failure" response if any,
// if not then a 400 bad request is being sent.
//
// Example usage at: https://github.com/kataras/iris/blob/master/mvc/method_result_test.go.
func Try(fn func() Result, failure ...Result) Result {
var failed bool
var actionResponse Result
func() {
defer func() {
if rec := recover(); rec != nil {
failed = true
}
}()
actionResponse = fn()
}()
if failed {
if len(failure) > 0 {
return failure[0]
}
return defaultFailureResponse
}
return actionResponse
}

View File

@ -1,69 +0,0 @@
package mvc
import (
"github.com/kataras/iris/context"
"github.com/kataras/iris/mvc/activator/methodfunc"
)
// Response completes the `methodfunc.Result` interface.
// It's being used as an alternative return value which
// wraps the status code, the content type, a content as bytes or as string
// and an error, it's smart enough to complete the request and send the correct response to the client.
type Response struct {
Code int
ContentType string
Content []byte
// if not empty then content type is the text/plain
// and content is the text as []byte.
Text string
// If not nil then it will fire that as "application/json" or the
// "ContentType" if not empty.
Object interface{}
// If Path is not empty then it will redirect
// the client to this Path, if Code is >= 300 and < 400
// then it will use that Code to do the redirection, otherwise
// StatusFound(302) or StatusSeeOther(303) for post methods will be used.
// Except when err != nil.
Path string
// if not empty then fire a 400 bad request error
// unless the Status is > 200, then fire that error code
// with the Err.Error() string as its content.
//
// if Err.Error() is empty then it fires the custom error handler
// if any otherwise the framework sends the default http error text based on the status.
Err error
Try func() int
// if true then it skips everything else and it throws a 404 not found error.
// Can be named as Failure but NotFound is more precise name in order
// to be visible that it's different than the `Err`
// because it throws a 404 not found instead of a 400 bad request.
// NotFound bool
// let's don't add this yet, it has its dangerous of missuse.
}
var _ methodfunc.Result = Response{}
// Dispatch writes the response result to the context's response writer.
func (r Response) Dispatch(ctx context.Context) {
if r.Path != "" && r.Err == nil {
// it's not a redirect valid status
if r.Code < 300 || r.Code >= 400 {
if ctx.Method() == "POST" {
r.Code = 303 // StatusSeeOther
}
r.Code = 302 // StatusFound
}
ctx.Redirect(r.Path, r.Code)
return
}
if s := r.Text; s != "" {
r.Content = []byte(s)
}
methodfunc.DispatchCommon(ctx, r.Code, r.ContentType, r.Content, r.Object, r.Err, true)
}

View File

@ -1,271 +0,0 @@
package mvc_test
import (
"errors"
"testing"
"github.com/kataras/iris"
"github.com/kataras/iris/context"
"github.com/kataras/iris/httptest"
"github.com/kataras/iris/mvc"
)
// activator/methodfunc/func_caller.go.
// and activator/methodfunc/func_result_dispatcher.go
type testControllerMethodResult struct {
mvc.C
}
func (c *testControllerMethodResult) Get() mvc.Result {
return mvc.Response{
Text: "Hello World!",
}
}
func (c *testControllerMethodResult) GetWithStatus() mvc.Response { // or mvc.Result again, no problem.
return mvc.Response{
Text: "This page doesn't exist",
Code: iris.StatusNotFound,
}
}
type testCustomStruct struct {
Name string `json:"name" xml:"name"`
Age int `json:"age" xml:"age"`
}
func (c *testControllerMethodResult) GetJson() mvc.Result {
var err error
if c.Ctx.URLParamExists("err") {
err = errors.New("error here")
}
return mvc.Response{
Err: err, // if err != nil then it will fire the error's text with a BadRequest.
Object: testCustomStruct{Name: "Iris", Age: 2},
}
}
var things = []string{"thing 0", "thing 1", "thing 2"}
func (c *testControllerMethodResult) GetThingWithTryBy(index int) mvc.Result {
failure := mvc.Response{
Text: "thing does not exist",
Code: iris.StatusNotFound,
}
return mvc.Try(func() mvc.Result {
// if panic because of index exceed the slice
// then the "failure" response will be returned instead.
return mvc.Response{Text: things[index]}
}, failure)
}
func (c *testControllerMethodResult) GetThingWithTryDefaultBy(index int) mvc.Result {
return mvc.Try(func() mvc.Result {
// if panic because of index exceed the slice
// then the default failure response will be returned instead (400 bad request).
return mvc.Response{Text: things[index]}
})
}
func TestControllerMethodResult(t *testing.T) {
app := iris.New()
app.Controller("/", new(testControllerMethodResult))
e := httptest.New(t, app)
e.GET("/").Expect().Status(iris.StatusOK).
Body().Equal("Hello World!")
e.GET("/with/status").Expect().Status(iris.StatusNotFound).
Body().Equal("This page doesn't exist")
e.GET("/json").Expect().Status(iris.StatusOK).
JSON().Equal(iris.Map{
"name": "Iris",
"age": 2,
})
e.GET("/json").WithQuery("err", true).Expect().
Status(iris.StatusBadRequest).
Body().Equal("error here")
e.GET("/thing/with/try/1").Expect().
Status(iris.StatusOK).
Body().Equal("thing 1")
// failure because of index exceed the slice
e.GET("/thing/with/try/3").Expect().
Status(iris.StatusNotFound).
Body().Equal("thing does not exist")
e.GET("/thing/with/try/default/3").Expect().
Status(iris.StatusBadRequest).
Body().Equal("Bad Request")
}
type testControllerMethodResultTypes struct {
mvc.Controller
}
func (c *testControllerMethodResultTypes) GetText() string {
return "text"
}
func (c *testControllerMethodResultTypes) GetStatus() int {
return iris.StatusBadGateway
}
func (c *testControllerMethodResultTypes) GetTextWithStatusOk() (string, int) {
return "OK", iris.StatusOK
}
// tests should have output arguments mixed
func (c *testControllerMethodResultTypes) GetStatusWithTextNotOkBy(first string, second string) (int, string) {
return iris.StatusForbidden, "NOT_OK_" + first + second
}
func (c *testControllerMethodResultTypes) GetTextAndContentType() (string, string) {
return "<b>text</b>", "text/html"
}
type testControllerMethodCustomResult struct {
HTML string
}
// The only one required function to make that a custom Response dispatcher.
func (r testControllerMethodCustomResult) Dispatch(ctx context.Context) {
ctx.HTML(r.HTML)
}
func (c *testControllerMethodResultTypes) GetCustomResponse() testControllerMethodCustomResult {
return testControllerMethodCustomResult{"<b>text</b>"}
}
func (c *testControllerMethodResultTypes) GetCustomResponseWithStatusOk() (testControllerMethodCustomResult, int) {
return testControllerMethodCustomResult{"<b>OK</b>"}, iris.StatusOK
}
func (c *testControllerMethodResultTypes) GetCustomResponseWithStatusNotOk() (testControllerMethodCustomResult, int) {
return testControllerMethodCustomResult{"<b>internal server error</b>"}, iris.StatusInternalServerError
}
func (c *testControllerMethodResultTypes) GetCustomStruct() testCustomStruct {
return testCustomStruct{"Iris", 2}
}
func (c *testControllerMethodResultTypes) GetCustomStructWithStatusNotOk() (testCustomStruct, int) {
return testCustomStruct{"Iris", 2}, iris.StatusInternalServerError
}
func (c *testControllerMethodResultTypes) GetCustomStructWithContentType() (testCustomStruct, string) {
return testCustomStruct{"Iris", 2}, "text/xml"
}
func (c *testControllerMethodResultTypes) GetCustomStructWithError() (s testCustomStruct, err error) {
s = testCustomStruct{"Iris", 2}
if c.Ctx.URLParamExists("err") {
err = errors.New("omit return of testCustomStruct and fire error")
}
// it should send the testCustomStruct as JSON if error is nil
// otherwise it should fire the default error(BadRequest) with the error's text.
return
}
func TestControllerMethodResultTypes(t *testing.T) {
app := iris.New()
app.Controller("/", new(testControllerMethodResultTypes))
e := httptest.New(t, app)
e.GET("/text").Expect().Status(iris.StatusOK).
Body().Equal("text")
e.GET("/status").Expect().Status(iris.StatusBadGateway)
e.GET("/text/with/status/ok").Expect().Status(iris.StatusOK).
Body().Equal("OK")
e.GET("/status/with/text/not/ok/first/second").Expect().Status(iris.StatusForbidden).
Body().Equal("NOT_OK_firstsecond")
e.GET("/text/and/content/type").Expect().Status(iris.StatusOK).
ContentType("text/html", "utf-8").
Body().Equal("<b>text</b>")
e.GET("/custom/response").Expect().Status(iris.StatusOK).
ContentType("text/html", "utf-8").
Body().Equal("<b>text</b>")
e.GET("/custom/response/with/status/ok").Expect().Status(iris.StatusOK).
ContentType("text/html", "utf-8").
Body().Equal("<b>OK</b>")
e.GET("/custom/response/with/status/not/ok").Expect().Status(iris.StatusInternalServerError).
ContentType("text/html", "utf-8").
Body().Equal("<b>internal server error</b>")
expectedResultFromCustomStruct := map[string]interface{}{
"name": "Iris",
"age": 2,
}
e.GET("/custom/struct").Expect().Status(iris.StatusOK).
JSON().Equal(expectedResultFromCustomStruct)
e.GET("/custom/struct/with/status/not/ok").Expect().Status(iris.StatusInternalServerError).
JSON().Equal(expectedResultFromCustomStruct)
e.GET("/custom/struct/with/content/type").Expect().Status(iris.StatusOK).
ContentType("text/xml", "utf-8")
e.GET("/custom/struct/with/error").Expect().Status(iris.StatusOK).
JSON().Equal(expectedResultFromCustomStruct)
e.GET("/custom/struct/with/error").WithQuery("err", true).Expect().
Status(iris.StatusBadRequest). // the default status code if error is not nil
// the content should be not JSON it should be the status code's text
// it will fire the error's text
Body().Equal("omit return of testCustomStruct and fire error")
}
type testControllerViewResultRespectCtxViewData struct {
T *testing.T
mvc.C
}
func (t *testControllerViewResultRespectCtxViewData) BeginRequest(ctx context.Context) {
t.C.BeginRequest(ctx)
ctx.ViewData("name_begin", "iris_begin")
}
func (t *testControllerViewResultRespectCtxViewData) EndRequest(ctx context.Context) {
t.C.EndRequest(ctx)
// check if data is not overridden by return mvc.View {Data: context.Map...}
dataWritten := ctx.GetViewData()
if dataWritten == nil {
t.T.Fatalf("view data is nil, both BeginRequest and Get failed to write the data")
return
}
if dataWritten["name_begin"] == nil {
t.T.Fatalf(`view data[name_begin] is nil,
BeginRequest's ctx.ViewData call have been overridden by Get's return mvc.View {Data: }.
Total view data: %v`, dataWritten)
}
if dataWritten["name"] == nil {
t.T.Fatalf("view data[name] is nil, Get's return mvc.View {Data: } didn't work. Total view data: %v", dataWritten)
}
}
func (t *testControllerViewResultRespectCtxViewData) Get() mvc.Result {
return mvc.View{
Name: "doesnt_exists.html",
Data: context.Map{"name": "iris"}, // we care about this only.
Code: iris.StatusInternalServerError,
}
}
func TestControllerViewResultRespectCtxViewData(t *testing.T) {
app := iris.New()
app.Controller("/", new(testControllerViewResultRespectCtxViewData), t)
e := httptest.New(t, app)
e.GET("/").Expect().Status(iris.StatusInternalServerError)
}

View File

@ -1,104 +0,0 @@
package mvc
import (
"strings"
"github.com/kataras/iris/context"
"github.com/kataras/iris/mvc/activator/methodfunc"
"github.com/fatih/structs"
)
// View completes the `methodfunc.Result` interface.
// It's being used as an alternative return value which
// wraps the template file name, layout, (any) view data, status code and error.
// It's smart enough to complete the request and send the correct response to the client.
//
// Example at: https://github.com/kataras/iris/blob/master/_examples/mvc/overview/web/controllers/hello_controller.go.
type View struct {
Name string
Layout string
Data interface{} // map or a custom struct.
Code int
Err error
}
var _ methodfunc.Result = View{}
const dotB = byte('.')
// DefaultViewExt is the default extension if `view.Name `is missing,
// but note that it doesn't care about
// the app.RegisterView(iris.$VIEW_ENGINE("./$dir", "$ext"))'s $ext.
// so if you don't use the ".html" as extension for your files
// you have to append the extension manually into the `view.Name`
// or change this global variable.
var DefaultViewExt = ".html"
func ensureExt(s string) string {
if len(s) == 0 {
return "index" + DefaultViewExt
}
if strings.IndexByte(s, dotB) < 1 {
s += DefaultViewExt
}
return s
}
// Dispatch writes the template filename, template layout and (any) data to the client.
// Completes the `Result` interface.
func (r View) Dispatch(ctx context.Context) { // r as Response view.
if r.Err != nil {
if r.Code < 400 {
r.Code = methodfunc.DefaultErrStatusCode
}
ctx.StatusCode(r.Code)
ctx.WriteString(r.Err.Error())
ctx.StopExecution()
return
}
if r.Code > 0 {
ctx.StatusCode(r.Code)
}
if r.Name != "" {
r.Name = ensureExt(r.Name)
if r.Layout != "" {
r.Layout = ensureExt(r.Layout)
ctx.ViewLayout(r.Layout)
}
if r.Data != nil {
// In order to respect any c.Ctx.ViewData that may called manually before;
dataKey := ctx.Application().ConfigurationReadOnly().GetViewDataContextKey()
if ctx.Values().Get(dataKey) == nil {
// if no c.Ctx.ViewData then it's empty do a
// pure set, it's faster.
ctx.Values().Set(dataKey, r.Data)
} else {
// else check if r.Data is map or struct, if struct convert it to map,
// do a range loop and set the data one by one.
// context.Map is actually a map[string]interface{} but we have to make that check;
if m, ok := r.Data.(map[string]interface{}); ok {
setViewData(ctx, m)
} else if m, ok := r.Data.(context.Map); ok {
setViewData(ctx, m)
} else if structs.IsStruct(r.Data) {
setViewData(ctx, structs.Map(r))
}
}
}
ctx.View(r.Name)
}
}
func setViewData(ctx context.Context, data map[string]interface{}) {
for k, v := range data {
ctx.ViewData(k, v)
}
}

View File

@ -1,47 +0,0 @@
package mvc
import (
"github.com/kataras/iris/context"
"github.com/kataras/iris/mvc/activator"
"github.com/kataras/iris/sessions"
"github.com/kataras/golog"
)
var defaultManager = sessions.New(sessions.Config{})
// SessionController is a simple `Controller` implementation
// which requires a binded session manager in order to give
// direct access to the current client's session via its `Session` field.
type SessionController struct {
C
Manager *sessions.Sessions
Session *sessions.Session
}
// OnActivate called, once per application lifecycle NOT request,
// every single time the dev registers a specific SessionController-based controller.
// It makes sure that its "Manager" field is filled
// even if the caller didn't provide any sessions manager via the `app.Controller` function.
func (s *SessionController) OnActivate(t *activator.TController) {
if !t.BindValueTypeExists(defaultManager) {
t.BindValue(defaultManager)
golog.Warnf(`MVC SessionController: couldn't find any "*sessions.Sessions" bindable value to fill the "Manager" field,
therefore this controller is using the default sessions manager instead.
Please refer to the documentation to learn how you can provide the session manager`)
}
}
// BeginRequest calls the Controller's BeginRequest
// and tries to initialize the current user's Session.
func (s *SessionController) BeginRequest(ctx context.Context) {
s.C.BeginRequest(ctx)
if s.Manager == nil {
ctx.Application().Logger().Errorf(`MVC SessionController: sessions manager is nil, report this as a bug
because the SessionController should predict this on its activation state and use a default one automatically`)
return
}
s.Session = s.Manager.Start(ctx)
}

View File

@ -1,38 +0,0 @@
package mvc
import (
"strings"
"unicode"
)
func findCtrlWords(ctrlName string) (w []string) {
end := len(ctrlName)
start := -1
for i, n := 0, end; i < n; i++ {
c := rune(ctrlName[i])
if unicode.IsUpper(c) {
// it doesn't count the last uppercase
if start != -1 {
end = i
w = append(w, strings.ToLower(ctrlName[start:end]))
}
start = i
continue
}
end = i + 1
}
// We can't omit the last name, we have to take it.
// because of controller names like
// "UserProfile", we need to return "user", "profile"
// if "UserController", we need to return "user"
// if "User", we need to return "user".
last := ctrlName[start:end]
if last == ctrlSuffix {
return
}
w = append(w, strings.ToLower(last))
return
}

View File

@ -1,31 +0,0 @@
package mvc
import (
"testing"
)
func TestFindCtrlWords(t *testing.T) {
var tests = map[string][]string{
"UserController": {"user"},
"UserPostController": {"user", "post"},
"ProfileController": {"profile"},
"UserProfileController": {"user", "profile"},
"UserProfilePostController": {"user", "profile", "post"},
"UserProfile": {"user", "profile"},
"Profile": {"profile"},
"User": {"user"},
}
for ctrlName, expected := range tests {
words := findCtrlWords(ctrlName)
if len(expected) != len(words) {
t.Fatalf("expected words and return don't have the same length: [%d] != [%d] | '%s' != '%s'",
len(expected), len(words), expected, words)
}
for i, w := range words {
if expected[i] != w {
t.Fatalf("expected word is not equal with the return one: '%s' != '%s'", expected[i], w)
}
}
}
}

View File

@ -1,7 +1,7 @@
package mvc2 package mvc2
import ( import (
"github.com/kataras/di" "github.com/kataras/iris/mvc2/di"
"reflect" "reflect"
) )

View File

@ -3,8 +3,9 @@ package mvc2
import ( import (
"fmt" "fmt"
"reflect" "reflect"
"strings"
"github.com/kataras/di" "github.com/kataras/iris/mvc2/di"
"github.com/kataras/iris/context" "github.com/kataras/iris/context"
"github.com/kataras/iris/core/router" "github.com/kataras/iris/core/router"
@ -75,9 +76,9 @@ func (c *C) EndRequest(ctx context.Context) {}
// ControllerActivator returns a new controller type info description. // ControllerActivator returns a new controller type info description.
// Its functionality can be overriden by the end-dev. // Its functionality can be overriden by the end-dev.
type ControllerActivator struct { type ControllerActivator struct {
// the router is used on the `Activate` and can be used by end-dev on the `OnActivate` // the router is used on the `Activate` and can be used by end-dev on the `BeforeActivate`
// to register any custom controller's functions as handlers but we will need it here // to register any custom controller's functions as handlers but we will need it here
// in order to not create a new type like `ActivationPayload` for the `OnActivate`. // in order to not create a new type like `ActivationPayload` for the `BeforeActivate`.
Router router.Party Router router.Party
// initRef BaseController // the BaseController as it's passed from the end-dev. // initRef BaseController // the BaseController as it's passed from the end-dev.
@ -88,7 +89,7 @@ type ControllerActivator struct {
FullName string FullName string
// the methods names that is already binded to a handler, // the methods names that is already binded to a handler,
// the BeginRequest, EndRequest and OnActivate are reserved by the internal implementation. // the BeginRequest, EndRequest and BeforeActivate are reserved by the internal implementation.
reservedMethods []string reservedMethods []string
// the bindings that comes from the Engine and the controller's filled fields if any. // the bindings that comes from the Engine and the controller's filled fields if any.
@ -100,6 +101,16 @@ type ControllerActivator struct {
injector *di.StructInjector injector *di.StructInjector
} }
func getNameOf(typ reflect.Type) string {
elemTyp := di.IndirectType(typ)
typName := elemTyp.Name()
pkgPath := elemTyp.PkgPath()
fullname := pkgPath[strings.LastIndexByte(pkgPath, '/')+1:] + "." + typName
return fullname
}
func newControllerActivator(router router.Party, controller interface{}, d *di.D) *ControllerActivator { func newControllerActivator(router router.Party, controller interface{}, d *di.D) *ControllerActivator {
var ( var (
val = reflect.ValueOf(controller) val = reflect.ValueOf(controller)
@ -116,7 +127,7 @@ func newControllerActivator(router router.Party, controller interface{}, d *di.D
// the end-developer when declaring the controller, // the end-developer when declaring the controller,
// activate listeners needs them in order to know if something set-ed already or not, // activate listeners needs them in order to know if something set-ed already or not,
// look `BindTypeExists`. // look `BindTypeExists`.
d.Values = append(lookupNonZeroFieldsValues(val), d.Values...) d.Values = append(di.LookupNonZeroFieldsValues(val), d.Values...)
c := &ControllerActivator{ c := &ControllerActivator{
// give access to the Router to the end-devs if they need it for some reason, // give access to the Router to the end-devs if they need it for some reason,
@ -142,7 +153,7 @@ func newControllerActivator(router router.Party, controller interface{}, d *di.D
} }
func whatReservedMethods(typ reflect.Type) []string { func whatReservedMethods(typ reflect.Type) []string {
methods := []string{"OnActivate"} methods := []string{"BeforeActivate"}
if isBaseController(typ) { if isBaseController(typ) {
methods = append(methods, "BeginRequest", "EndRequest") methods = append(methods, "BeginRequest", "EndRequest")
} }
@ -182,7 +193,6 @@ func (c *ControllerActivator) parseMethods() {
} }
func (c *ControllerActivator) activate() { func (c *ControllerActivator) activate() {
c.injector = c.Dependencies.Struct(c.Value)
c.parseMethods() c.parseMethods()
} }
@ -233,18 +243,40 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware .
pathParams := getPathParamsForInput(tmpl.Params, funcIn[1:]...) pathParams := getPathParamsForInput(tmpl.Params, funcIn[1:]...)
// get the function's input arguments' bindings. // get the function's input arguments' bindings.
funcDependencies := c.Dependencies.Clone() funcDependencies := c.Dependencies.Clone()
funcDependencies.Add(pathParams...) funcDependencies.AddValue(pathParams...)
funcInjector := funcDependencies.Func(m.Func) funcInjector := funcDependencies.Func(m.Func)
// the element value, not the pointer, wil lbe used to create a
// new controller on each incoming request.
// Remember:
// we cannot simply do that and expect to work:
// hasStructInjector = c.injector != nil && c.injector.Valid
// hasFuncInjector = funcInjector != nil && funcInjector.Valid
// because
// the `Handle` can be called from `BeforeActivate` callbacks
// and before activation, the c.injector is nil because
// we may not have the dependencies binded yet. But if `c.injector.Valid`
// inside the Handelr works because it's set on the `activate()` method.
// To solve this we can make check on the FIRST `Handle`,
// if c.injector is nil, then set it with the current bindings,
// so the user should bind the dependencies needed before the `Handle`
// this is a logical flow, so we will choose that one ->
if c.injector == nil {
c.injector = c.Dependencies.Struct(c.Value)
}
var (
hasStructInjector = c.injector != nil && c.injector.Valid
hasFuncInjector = funcInjector != nil && funcInjector.Valid
implementsBase = isBaseController(c.Type)
// we will make use of 'n' to make a slice of reflect.Value // we will make use of 'n' to make a slice of reflect.Value
// to pass into if the function has input arguments that // to pass into if the function has input arguments that
// are will being filled by the funcDependencies. // are will being filled by the funcDependencies.
n := len(funcIn) n = len(funcIn)
// the element value, not the pointer, wil lbe used to create a
// new controller on each incoming request.
elemTyp := indirectTyp(c.Type)
implementsBase := isBaseController(c.Type) elemTyp = di.IndirectType(c.Type)
)
handler := func(ctx context.Context) { handler := func(ctx context.Context) {
ctrl := reflect.New(elemTyp) ctrl := reflect.New(elemTyp)
@ -263,11 +295,11 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware .
defer b.EndRequest(ctx) defer b.EndRequest(ctx)
} }
if !c.injector.Valid && !funcInjector.Valid { if !hasStructInjector && !hasFuncInjector {
DispatchFuncResult(ctx, ctrl.Method(m.Index).Call(emptyIn)) DispatchFuncResult(ctx, ctrl.Method(m.Index).Call(emptyIn))
} else { } else {
ctxValue := reflect.ValueOf(ctx) ctxValue := reflect.ValueOf(ctx)
if c.injector.Valid { if hasStructInjector {
elem := ctrl.Elem() elem := ctrl.Elem()
c.injector.InjectElem(elem, ctxValue) c.injector.InjectElem(elem, ctxValue)
if ctx.IsStopped() { if ctx.IsStopped() {
@ -276,13 +308,13 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware .
// we do this in order to reduce in := make... // we do this in order to reduce in := make...
// if not func input binders, we execute the handler with empty input args. // if not func input binders, we execute the handler with empty input args.
if !funcInjector.Valid { if !hasFuncInjector {
DispatchFuncResult(ctx, ctrl.Method(m.Index).Call(emptyIn)) DispatchFuncResult(ctx, ctrl.Method(m.Index).Call(emptyIn))
} }
} }
// otherwise, it has one or more valid input binders, // otherwise, it has one or more valid input binders,
// make the input and call the func using those. // make the input and call the func using those.
if funcInjector.Valid { if hasFuncInjector {
in := make([]reflect.Value, n, n) in := make([]reflect.Value, n, n)
in[0] = ctrl in[0] = ctrl
funcInjector.Inject(&in, ctxValue) funcInjector.Inject(&in, ctxValue)

View File

@ -24,12 +24,11 @@ func (c *testControllerHandle) BeginRequest(ctx iris.Context) {
c.reqField = ctx.URLParam("reqfield") c.reqField = ctx.URLParam("reqfield")
} }
func (c *testControllerHandle) OnActivate(t *ControllerActivator) { // OnActivate(t *mvc.TController) { func (c *testControllerHandle) BeforeActivate(ca *ControllerActivator) { // BeforeActivate(t *mvc.TController) {
// t.Handle("GET", "/", "Get") ca.Handle("GET", "/histatic", "HiStatic")
t.Handle("GET", "/histatic", "HiStatic") ca.Handle("GET", "/hiservice", "HiService")
t.Handle("GET", "/hiservice", "HiService") ca.Handle("GET", "/hiparam/{ps:string}", "HiParamBy")
t.Handle("GET", "/hiparam/{ps:string}", "HiParamBy") ca.Handle("GET", "/hiparamempyinput/{ps:string}", "HiParamEmptyInputBy")
t.Handle("GET", "/hiparamempyinput/{ps:string}", "HiParamEmptyInputBy")
} }
func (c *testControllerHandle) HiStatic() string { func (c *testControllerHandle) HiStatic() string {
@ -51,8 +50,10 @@ func (c *testControllerHandle) HiParamEmptyInputBy() string {
func TestControllerHandle(t *testing.T) { func TestControllerHandle(t *testing.T) {
app := iris.New() app := iris.New()
m := New() m := NewEngine()
m.Bind(&TestServiceImpl{prefix: "service:"}).Controller(app, new(testControllerHandle)) m.Dependencies.Add(&TestServiceImpl{prefix: "service:"})
m.Controller(app, new(testControllerHandle))
e := httptest.New(t, app) e := httptest.New(t, app)
// test the index, is not part of the current package's implementation but do it. // test the index, is not part of the current package's implementation but do it.

View File

@ -63,7 +63,7 @@ func (c *testControllerAny) Any() {
func TestControllerMethodFuncs(t *testing.T) { func TestControllerMethodFuncs(t *testing.T) {
app := iris.New() app := iris.New()
m := New() m := NewEngine()
m.Controller(app, new(testController)) m.Controller(app, new(testController))
m.Controller(app.Party("/all"), new(testControllerAll)) m.Controller(app.Party("/all"), new(testControllerAll))
m.Controller(app.Party("/any"), new(testControllerAny)) m.Controller(app.Party("/any"), new(testControllerAny))
@ -113,7 +113,7 @@ func (c *testControllerBeginAndEndRequestFunc) Post() {
func TestControllerBeginAndEndRequestFunc(t *testing.T) { func TestControllerBeginAndEndRequestFunc(t *testing.T) {
app := iris.New() app := iris.New()
New().Controller(app.Party("/profile/{username}"), new(testControllerBeginAndEndRequestFunc)) NewEngine().Controller(app.Party("/profile/{username}"), new(testControllerBeginAndEndRequestFunc))
e := httptest.New(t, app) e := httptest.New(t, app)
usernames := []string{ usernames := []string{
@ -156,7 +156,7 @@ func TestControllerBeginAndEndRequestFuncBindMiddleware(t *testing.T) {
ctx.Writef("forbidden") ctx.Writef("forbidden")
} }
New().Controller(app.Party("/profile/{username}", middlewareCheck), NewEngine().Controller(app.Party("/profile/{username}", middlewareCheck),
new(testControllerBeginAndEndRequestFunc)) new(testControllerBeginAndEndRequestFunc))
e := httptest.New(t, app) e := httptest.New(t, app)
@ -230,7 +230,7 @@ func (c *testControllerEndRequestAwareness) EndRequest(ctx context.Context) {
func TestControllerEndRequestAwareness(t *testing.T) { func TestControllerEndRequestAwareness(t *testing.T) {
app := iris.New() app := iris.New()
New().Controller(app.Party("/era/{username}"), new(testControllerEndRequestAwareness)) NewEngine().Controller(app.Party("/era/{username}"), new(testControllerEndRequestAwareness))
e := httptest.New(t, app) e := httptest.New(t, app)
usernames := []string{ usernames := []string{
@ -284,8 +284,8 @@ func TestControllerBind(t *testing.T) {
myTitlePtr := &testBindType{title: t1} myTitlePtr := &testBindType{title: t1}
// test bind value to value of the correct type // test bind value to value of the correct type
myTitleV := testBindType{title: t2} myTitleV := testBindType{title: t2}
m := New() m := NewEngine()
m.Bind(myTitlePtr, myTitleV) m.Dependencies.Add(myTitlePtr, myTitleV)
// or just app // or just app
m.Controller(app.Party("/"), new(testControllerBindStruct)) m.Controller(app.Party("/"), new(testControllerBindStruct))
m.Controller(app.Party("/deep"), new(testControllerBindDeep)) m.Controller(app.Party("/deep"), new(testControllerBindDeep))
@ -345,8 +345,9 @@ func TestControllerInsideControllerRecursively(t *testing.T) {
) )
app := iris.New() app := iris.New()
New().Bind(&testBindType{title: title}). m := NewEngine()
Controller(app.Party("/user/{username}"), new(testCtrl0)) m.Dependencies.Add(&testBindType{title: title})
m.Controller(app.Party("/user/{username}"), new(testCtrl0))
e := httptest.New(t, app) e := httptest.New(t, app)
e.GET("/user/" + username).Expect(). e.GET("/user/" + username).Expect().
@ -378,7 +379,7 @@ func (c *testControllerRelPathFromFunc) GetSomethingByElseThisBy(bool, int) {} /
func TestControllerRelPathFromFunc(t *testing.T) { func TestControllerRelPathFromFunc(t *testing.T) {
app := iris.New() app := iris.New()
New().Controller(app, new(testControllerRelPathFromFunc)) NewEngine().Controller(app, new(testControllerRelPathFromFunc))
e := httptest.New(t, app) e := httptest.New(t, app)
e.GET("/").Expect().Status(iris.StatusOK). e.GET("/").Expect().Status(iris.StatusOK).
@ -420,12 +421,8 @@ type testControllerActivateListener struct {
TitlePointer *testBindType TitlePointer *testBindType
} }
func (c *testControllerActivateListener) OnActivate(ca *ControllerActivator) { func (c *testControllerActivateListener) BeforeActivate(ca *ControllerActivator) {
if !ca.Dependencies.BindExists(&testBindType{}) { ca.Dependencies.AddOnce(&testBindType{title: "default title"})
ca.Dependencies.Bind(&testBindType{
title: "default title",
})
}
} }
func (c *testControllerActivateListener) Get() string { func (c *testControllerActivateListener) Get() string {
@ -434,12 +431,14 @@ func (c *testControllerActivateListener) Get() string {
func TestControllerActivateListener(t *testing.T) { func TestControllerActivateListener(t *testing.T) {
app := iris.New() app := iris.New()
New().Controller(app, new(testControllerActivateListener)) NewEngine().Controller(app, new(testControllerActivateListener))
New().Bind(&testBindType{ // will bind to all controllers under this .New() MVC Engine. m := NewEngine()
m.Dependencies.Add(&testBindType{ // will bind to all controllers under this .New() MVC Engine.
title: "my title", title: "my title",
}).Controller(app.Party("/manual"), new(testControllerActivateListener)) })
m.Controller(app.Party("/manual"), new(testControllerActivateListener))
// or // or
New().Controller(app.Party("/manual2"), &testControllerActivateListener{ NewEngine().Controller(app.Party("/manual2"), &testControllerActivateListener{
TitlePointer: &testBindType{ TitlePointer: &testBindType{
title: "my title", title: "my title",
}, },

92
mvc2/di/di.go Normal file
View File

@ -0,0 +1,92 @@
package di
import "reflect"
type (
// Hijacker is a type which is used to catch fields or function's input argument
// to bind a custom object based on their type.
Hijacker func(reflect.Type) (*BindObject, bool)
// TypeChecker checks if a specific field's or function input argument's
// is valid to be binded.
TypeChecker func(reflect.Type) bool
)
// D is the Dependency Injection container,
// it contains the Values that can be changed before the injectors.
// `Struct` and the `Func` methods returns an injector for specific
// struct instance-value or function.
type D struct {
Values
hijacker Hijacker
goodFunc TypeChecker
}
// New creates and returns a new Dependency Injection container.
// See `Values` field and `Func` and `Struct` methods for more.
func New() *D {
return &D{}
}
// Hijack sets a hijacker function, read the `Hijacker` type for more explaination.
func (d *D) Hijack(fn Hijacker) *D {
d.hijacker = fn
return d
}
// GoodFunc sets a type checker for a valid function that can be binded,
// read the `TypeChecker` type for more explaination.
func (d *D) GoodFunc(fn TypeChecker) *D {
d.goodFunc = fn
return d
}
// Clone returns a new Dependency Injection container, it adopts the
// parent's (current "D") hijacker, good func type checker and all dependencies values.
func (d *D) Clone() *D {
clone := New()
clone.hijacker = d.hijacker
clone.goodFunc = d.goodFunc
// copy the current dynamic bindings (func binders)
// and static struct bindings (services) to this new child.
if n := len(d.Values); n > 0 {
values := make(Values, n, n)
copy(values, d.Values)
clone.Values = values
}
return clone
}
// Struct is being used to return a new injector based on
// a struct value instance, if it contains fields that the types of those
// are matching with one or more of the `Values` then they are binded
// with the injector's `Inject` and `InjectElem` methods.
func (d *D) Struct(s interface{}) *StructInjector {
if s == nil {
return nil
}
v := ValueOf(s)
return MakeStructInjector(
v,
d.hijacker,
d.goodFunc,
d.Values...,
)
}
// Func is being used to return a new injector based on
// a function, if it contains input arguments that the types of those
// are matching with one or more of the `Values` then they are binded
// to the function's input argument when called
// with the injector's `Fill` method.
func (d *D) Func(fn interface{}) *FuncInjector {
return MakeFuncInjector(
ValueOf(fn),
d.hijacker,
d.goodFunc,
d.Values...,
)
}

108
mvc2/di/func.go Normal file
View File

@ -0,0 +1,108 @@
package di
import (
"reflect"
)
type (
targetFuncInput struct {
Object *BindObject
InputIndex int
}
FuncInjector struct {
// the original function, is being used
// only the .Call, which is refering to the same function, always.
fn reflect.Value
inputs []*targetFuncInput
// Length is the number of the valid, final binded input arguments.
Length int
// Valid is True when `Length` is > 0, it's statically set-ed for
// performance reasons.
Valid bool //
}
)
func MakeFuncInjector(fn reflect.Value, hijack Hijacker, goodFunc TypeChecker, values ...reflect.Value) *FuncInjector {
typ := IndirectType(fn.Type())
s := &FuncInjector{
fn: fn,
}
if !IsFunc(typ) {
return s
}
n := typ.NumIn()
// function input can have many values of the same types,
// so keep track of them in order to not set a func input to a next bind value,
// i.e (string, string) with two different binder funcs because of the different param's name.
consumedValues := make(map[int]bool, n)
for i := 0; i < n; i++ {
inTyp := typ.In(i)
if hijack != nil {
if b, ok := hijack(inTyp); ok && b != nil {
s.inputs = append(s.inputs, &targetFuncInput{
InputIndex: i,
Object: b,
})
continue
}
}
for valIdx, val := range values {
if _, shouldSkip := consumedValues[valIdx]; shouldSkip {
continue
}
inTyp := typ.In(i)
// the binded values to the func's inputs.
b, err := MakeBindObject(val, goodFunc)
if err != nil {
return s // if error stop here.
}
if b.IsAssignable(inTyp) {
// fmt.Printf("binded input index: %d for type: %s and value: %v with pointer: %v\n",
// i, b.Type.String(), val.String(), val.Pointer())
s.inputs = append(s.inputs, &targetFuncInput{
InputIndex: i,
Object: &b,
})
consumedValues[valIdx] = true
break
}
}
}
s.Length = n
s.Valid = len(s.inputs) > 0
return s
}
func (s *FuncInjector) Inject(in *[]reflect.Value, ctx ...reflect.Value) {
args := *in
for _, input := range s.inputs {
input.Object.Assign(ctx, func(v reflect.Value) {
// fmt.Printf("assign input index: %d for value: %v\n",
// input.InputIndex, v.String())
args[input.InputIndex] = v
})
}
*in = args
}
func (s *FuncInjector) Call(ctx ...reflect.Value) []reflect.Value {
in := make([]reflect.Value, s.Length, s.Length)
s.Inject(&in, ctx...)
return s.fn.Call(in)
}

97
mvc2/di/object.go Normal file
View File

@ -0,0 +1,97 @@
package di
import (
"errors"
"reflect"
)
type BindType uint32
const (
Static BindType = iota // simple assignable value, a static value.
Dynamic // dynamic value, depends on some input arguments from the caller.
)
type BindObject struct {
Type reflect.Type // the Type of 'Value' or the type of the returned 'ReturnValue' .
Value reflect.Value
BindType BindType
ReturnValue func([]reflect.Value) reflect.Value
}
func MakeBindObject(v reflect.Value, goodFunc TypeChecker) (b BindObject, err error) {
if IsFunc(v) {
b.BindType = Dynamic
b.ReturnValue, b.Type, err = MakeReturnValue(v, goodFunc)
} else {
b.BindType = Static
b.Type = v.Type()
b.Value = v
}
return
}
var errBad = errors.New("bad")
// MakeReturnValue takes any function
// that accept custom values and returns something,
// it returns a binder function, which accepts a slice of reflect.Value
// and returns a single one reflect.Value for that.
// It's being used to resolve the input parameters on a "x" consumer faster.
//
// The "fn" can have the following form:
// `func(myService) MyViewModel`.
//
// The return type of the "fn" should be a value instance, not a pointer, for your own protection.
// The binder function should return only one value.
func MakeReturnValue(fn reflect.Value, goodFunc TypeChecker) (func([]reflect.Value) reflect.Value, reflect.Type, error) {
typ := IndirectType(fn.Type())
// invalid if not a func.
if typ.Kind() != reflect.Func {
return nil, typ, errBad
}
// invalid if not returns one single value.
if typ.NumOut() != 1 {
return nil, typ, errBad
}
if goodFunc != nil {
if !goodFunc(typ) {
return nil, typ, errBad
}
}
outTyp := typ.Out(0)
zeroOutVal := reflect.New(outTyp).Elem()
bf := func(ctxValue []reflect.Value) reflect.Value {
results := fn.Call(ctxValue)
if len(results) == 0 {
return zeroOutVal
}
v := results[0]
if !v.IsValid() {
return zeroOutVal
}
return v
}
return bf, outTyp, nil
}
func (b *BindObject) IsAssignable(to reflect.Type) bool {
return equalTypes(b.Type, to)
}
func (b *BindObject) Assign(ctx []reflect.Value, toSetter func(reflect.Value)) {
if b.BindType == Dynamic {
toSetter(b.ReturnValue(ctx))
return
}
toSetter(b.Value)
}

180
mvc2/di/reflect.go Normal file
View File

@ -0,0 +1,180 @@
package di
import "reflect"
var emptyIn = []reflect.Value{}
// IsZero returns true if a value is nil, remember boolean's false is zero.
// Remember; fields to be checked should be exported otherwise it returns false.
func IsZero(v reflect.Value) bool {
switch v.Kind() {
case reflect.Struct:
zero := true
for i := 0; i < v.NumField(); i++ {
zero = zero && IsZero(v.Field(i))
}
if typ := v.Type(); typ != nil && v.IsValid() {
f, ok := typ.MethodByName("IsZero")
// if not found
// if has input arguments (1 is for the value receiver, so > 1 for the actual input args)
// if output argument is not boolean
// then skip this IsZero user-defined function.
if !ok || f.Type.NumIn() > 1 || f.Type.NumOut() != 1 && f.Type.Out(0).Kind() != reflect.Bool {
return zero
}
method := v.Method(f.Index)
// no needed check but:
if method.IsValid() && !method.IsNil() {
// it shouldn't panic here.
zero = method.Call(emptyIn)[0].Interface().(bool)
}
}
return zero
case reflect.Func, reflect.Map, reflect.Slice:
return v.IsNil()
case reflect.Array:
zero := true
for i := 0; i < v.Len(); i++ {
zero = zero && IsZero(v.Index(i))
}
return zero
}
// if not any special type then use the reflect's .Zero
// usually for fields, but remember if it's boolean and it's false
// then it's zero, even if set-ed.
if !v.CanInterface() {
// if can't interface, i.e return value from unexported field or method then return false
return false
}
zero := reflect.Zero(v.Type())
return v.Interface() == zero.Interface()
}
func IndirectValue(v reflect.Value) reflect.Value {
return reflect.Indirect(v)
}
func ValueOf(o interface{}) reflect.Value {
if v, ok := o.(reflect.Value); ok {
return v
}
return reflect.ValueOf(o)
}
func IndirectType(typ reflect.Type) reflect.Type {
switch typ.Kind() {
case reflect.Ptr, reflect.Array, reflect.Chan, reflect.Map, reflect.Slice:
return typ.Elem()
}
return typ
}
func goodVal(v reflect.Value) bool {
switch v.Kind() {
case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Interface, reflect.Slice:
if v.IsNil() {
return false
}
}
return v.IsValid()
}
func IsFunc(kindable interface {
Kind() reflect.Kind
}) bool {
return kindable.Kind() == reflect.Func
}
func equalTypes(got reflect.Type, expected reflect.Type) bool {
if got == expected {
return true
}
// if accepts an interface, check if the given "got" type does
// implement this "expected" user handler's input argument.
if expected.Kind() == reflect.Interface {
// fmt.Printf("expected interface = %s and got to set on the arg is: %s\n", expected.String(), got.String())
return got.Implements(expected)
}
return false
}
// for controller's fields only.
func structFieldIgnored(f reflect.StructField) bool {
if !f.Anonymous {
return true // if not anonymous(embedded), ignore it.
}
s := f.Tag.Get("ignore")
return s == "true" // if has an ignore tag then ignore it.
}
type field struct {
Type reflect.Type
Index []int // the index of the field, slice if it's part of a embedded struct
Name string // the actual name
// this could be empty, but in our cases it's not,
// it's filled with the bind object (as service which means as static value)
// and it's filled from the lookupFields' caller.
AnyValue reflect.Value
}
func lookupFields(elemTyp reflect.Type, parentIndex []int) (fields []field) {
if elemTyp.Kind() != reflect.Struct {
return
}
for i, n := 0, elemTyp.NumField(); i < n; i++ {
f := elemTyp.Field(i)
if IndirectType(f.Type).Kind() == reflect.Struct &&
!structFieldIgnored(f) {
fields = append(fields, lookupFields(f.Type, append(parentIndex, i))...)
continue
}
// skip unexported fields here,
// after the check for embedded structs, these can be binded if their
// fields are exported.
if f.PkgPath != "" {
continue
}
index := []int{i}
if len(parentIndex) > 0 {
index = append(parentIndex, i)
}
field := field{
Type: f.Type,
Name: f.Name,
Index: index,
}
fields = append(fields, field)
}
return
}
// LookupNonZeroFieldsValues lookup for filled fields based on the "v" struct value instance.
// It returns a slice of reflect.Value (same type as `Values`) that can be binded,
// like the end-developer's custom values.
func LookupNonZeroFieldsValues(v reflect.Value) (bindValues []reflect.Value) {
elem := IndirectValue(v)
fields := lookupFields(IndirectType(v.Type()), nil)
for _, f := range fields {
if fieldVal := elem.FieldByIndex(f.Index); f.Type.Kind() == reflect.Ptr && !IsZero(fieldVal) {
bindValues = append(bindValues, fieldVal)
}
}
return
}

84
mvc2/di/struct.go Normal file
View File

@ -0,0 +1,84 @@
package di
import "reflect"
type (
targetStructField struct {
Object *BindObject
FieldIndex []int
}
StructInjector struct {
elemType reflect.Type
//
fields []*targetStructField
Valid bool // is True when contains fields and it's a valid target struct.
}
)
func MakeStructInjector(v reflect.Value, hijack Hijacker, goodFunc TypeChecker, values ...reflect.Value) *StructInjector {
s := &StructInjector{
elemType: IndirectType(v.Type()),
}
fields := lookupFields(s.elemType, nil)
for _, f := range fields {
if hijack != nil {
if b, ok := hijack(f.Type); ok && b != nil {
s.fields = append(s.fields, &targetStructField{
FieldIndex: f.Index,
Object: b,
})
continue
}
}
for _, val := range values {
// the binded values to the struct's fields.
b, err := MakeBindObject(val, goodFunc)
if err != nil {
return s // if error stop here.
}
if b.IsAssignable(f.Type) {
// fmt.Printf("bind the object to the field: %s at index: %#v and type: %s\n", f.Name, f.Index, f.Type.String())
s.fields = append(s.fields, &targetStructField{
FieldIndex: f.Index,
Object: &b,
})
break
}
}
}
s.Valid = len(s.fields) > 0
return s
}
func (s *StructInjector) Inject(dest interface{}, ctx ...reflect.Value) {
if dest == nil {
return
}
v := IndirectValue(ValueOf(dest))
s.InjectElem(v, ctx...)
}
func (s *StructInjector) InjectElem(destElem reflect.Value, ctx ...reflect.Value) {
for _, f := range s.fields {
f.Object.Assign(ctx, func(v reflect.Value) {
// fmt.Printf("%s for %s at index: %d\n", destElem.Type().String(), f.Object.Type.String(), f.FieldIndex)
destElem.FieldByIndex(f.FieldIndex).Set(v)
})
}
}
func (s *StructInjector) New(ctx ...reflect.Value) reflect.Value {
dest := reflect.New(s.elemType)
s.InjectElem(dest, ctx...)
return dest
}

100
mvc2/di/values.go Normal file
View File

@ -0,0 +1,100 @@
package di
import (
"reflect"
)
type Values []reflect.Value
func NewValues() Values {
return Values{}
}
// Add binds values to this controller, if you want to share
// binding values between controllers use the Engine's `Bind` function instead.
func (bv *Values) Add(values ...interface{}) {
for _, val := range values {
bv.AddValue(reflect.ValueOf(val))
}
}
// AddValue same as `Add` but accepts reflect.Value
// instead.
func (bv *Values) AddValue(values ...reflect.Value) {
for _, v := range values {
if !goodVal(v) {
continue
}
*bv = append(*bv, v)
}
}
// Remove unbinds a binding value based on the type,
// it returns true if at least one field is not binded anymore.
//
// The "n" indicates the number of elements to remove, if <=0 then it's 1,
// this is useful because you may have bind more than one value to two or more fields
// with the same type.
func (bv *Values) Remove(value interface{}, n int) bool {
return bv.remove(reflect.TypeOf(value), n)
}
func (bv *Values) remove(typ reflect.Type, n int) (ok bool) {
input := *bv
for i, in := range input {
if equalTypes(in.Type(), typ) {
ok = true
input = input[:i+copy(input[i:], input[i+1:])]
if n > 1 {
continue
}
break
}
}
*bv = input
return
}
// Has returns true if a binder responsible to
// bind and return a type of "typ" is already registered to this controller.
func (bv *Values) Has(value interface{}) bool {
return bv.valueTypeExists(reflect.TypeOf(value))
}
func (bv *Values) valueTypeExists(typ reflect.Type) bool {
input := *bv
for _, in := range input {
if equalTypes(in.Type(), typ) {
return true
}
}
return false
}
// AddOnce binds a value to the controller's field with the same type,
// if it's not binded already.
//
// Returns false if binded already or the value is not the proper one for binding,
// otherwise true.
func (bv *Values) AddOnce(value interface{}) bool {
return bv.addIfNotExists(reflect.ValueOf(value))
}
func (bv *Values) addIfNotExists(v reflect.Value) bool {
var (
typ = v.Type() // no element, raw things here.
)
if !goodVal(v) {
return false
}
if bv.valueTypeExists(typ) {
return false
}
bv.AddValue(v)
return true
}

View File

@ -1,65 +1,52 @@
package mvc2 package mvc2
import ( import (
"errors" "github.com/kataras/iris/mvc2/di"
"github.com/kataras/di"
"github.com/kataras/golog" "github.com/kataras/golog"
"github.com/kataras/iris/context" "github.com/kataras/iris/context"
"github.com/kataras/iris/core/router" "github.com/kataras/iris/core/router"
) )
var (
errNil = errors.New("nil")
errBad = errors.New("bad")
errAlreadyExists = errors.New("already exists")
)
type Engine struct { type Engine struct {
dependencies *di.D Dependencies *di.D
} }
func New() *Engine { func NewEngine() *Engine {
return &Engine{ return &Engine{
dependencies: di.New().Hijack(hijacker).GoodFunc(typeChecker), Dependencies: di.New().Hijack(hijacker).GoodFunc(typeChecker),
} }
} }
func (e *Engine) Bind(values ...interface{}) *Engine { func (e *Engine) Clone() *Engine {
e.dependencies.Bind(values...) child := NewEngine()
return e child.Dependencies = e.Dependencies.Clone()
}
func (e *Engine) Child() *Engine {
child := New()
child.dependencies = e.dependencies.Clone()
return child return child
} }
func (e *Engine) Handler(handler interface{}) context.Handler { func (e *Engine) Handler(handler interface{}) context.Handler {
h, err := MakeHandler(handler, e.dependencies.Values...) h, err := MakeHandler(handler, e.Dependencies.Values...)
if err != nil { if err != nil {
golog.Errorf("mvc handler: %v", err) golog.Errorf("mvc handler: %v", err)
} }
return h return h
} }
func (e *Engine) Controller(router router.Party, controller interface{}, onActivate ...func(*ControllerActivator)) { func (e *Engine) Controller(router router.Party, controller interface{}, beforeActivate ...func(*ControllerActivator)) {
ca := newControllerActivator(router, controller, e.dependencies) ca := newControllerActivator(router, controller, e.Dependencies)
// give a priority to the "onActivate" // give a priority to the "beforeActivate"
// callbacks, if any. // callbacks, if any.
for _, cb := range onActivate { for _, cb := range beforeActivate {
cb(ca) cb(ca)
} }
// check if controller has an "OnActivate" function // check if controller has an "BeforeActivate" function
// which accepts the controller activator and call it. // which accepts the controller activator and call it.
if activateListener, ok := controller.(interface { if activateListener, ok := controller.(interface {
OnActivate(*ControllerActivator) BeforeActivate(*ControllerActivator)
}); ok { }); ok {
activateListener.OnActivate(ca) activateListener.BeforeActivate(ca)
} }
ca.activate() ca.activate()

View File

@ -9,7 +9,8 @@ import (
) )
func TestMvcEngineInAndHandler(t *testing.T) { func TestMvcEngineInAndHandler(t *testing.T) {
m := New().Bind(testBinderFuncUserStruct, testBinderService, testBinderFuncParam) m := NewEngine()
m.Dependencies.Add(testBinderFuncUserStruct, testBinderService, testBinderFuncParam)
var ( var (
h1 = m.Handler(testConsumeUserHandler) h1 = m.Handler(testConsumeUserHandler)

View File

@ -5,6 +5,8 @@ import (
"strings" "strings"
"github.com/fatih/structs" "github.com/fatih/structs"
"github.com/kataras/iris/mvc2/di"
"github.com/kataras/iris/context" "github.com/kataras/iris/context"
) )
@ -405,7 +407,7 @@ func (r View) Dispatch(ctx context.Context) { // r as Response view.
setViewData(ctx, m) setViewData(ctx, m)
} else if m, ok := r.Data.(context.Map); ok { } else if m, ok := r.Data.(context.Map); ok {
setViewData(ctx, m) setViewData(ctx, m)
} else if indirectVal(reflect.ValueOf(r.Data)).Kind() == reflect.Struct { } else if di.IndirectValue(reflect.ValueOf(r.Data)).Kind() == reflect.Struct {
setViewData(ctx, structs.Map(r)) setViewData(ctx, structs.Map(r))
} }
} }

View File

@ -71,7 +71,7 @@ func (c *testControllerMethodResult) GetThingWithTryDefaultBy(index int) Result
func TestControllerMethodResult(t *testing.T) { func TestControllerMethodResult(t *testing.T) {
app := iris.New() app := iris.New()
New().Controller(app, new(testControllerMethodResult)) NewEngine().Controller(app, new(testControllerMethodResult))
e := httptest.New(t, app) e := httptest.New(t, app)
@ -175,7 +175,7 @@ func (c *testControllerMethodResultTypes) GetCustomStructWithError() (s testCust
func TestControllerMethodResultTypes(t *testing.T) { func TestControllerMethodResultTypes(t *testing.T) {
app := iris.New() app := iris.New()
New().Controller(app, new(testControllerMethodResultTypes)) NewEngine().Controller(app, new(testControllerMethodResultTypes))
e := httptest.New(t, app, httptest.LogLevel("debug")) e := httptest.New(t, app, httptest.LogLevel("debug"))
@ -266,8 +266,8 @@ func (t *testControllerViewResultRespectCtxViewData) Get() Result {
func TestControllerViewResultRespectCtxViewData(t *testing.T) { func TestControllerViewResultRespectCtxViewData(t *testing.T) {
app := iris.New() app := iris.New()
New().Controller(app, new(testControllerViewResultRespectCtxViewData), func(ca *ControllerActivator) { NewEngine().Controller(app, new(testControllerViewResultRespectCtxViewData), func(ca *ControllerActivator) {
ca.Dependencies.Bind(t) ca.Dependencies.Add(t)
}) })
e := httptest.New(t, app) e := httptest.New(t, app)

View File

@ -2,7 +2,7 @@ package mvc2
import ( import (
"fmt" "fmt"
"github.com/kataras/di" "github.com/kataras/iris/mvc2/di"
"reflect" "reflect"
"runtime" "runtime"
@ -23,7 +23,7 @@ func isContextHandler(handler interface{}) (context.Handler, bool) {
} }
func validateHandler(handler interface{}) error { func validateHandler(handler interface{}) error {
if typ := reflect.TypeOf(handler); !isFunc(typ) { if typ := reflect.TypeOf(handler); !di.IsFunc(typ) {
return fmt.Errorf("handler expected to be a kind of func but got typeof(%s)", typ.String()) return fmt.Errorf("handler expected to be a kind of func but got typeof(%s)", typ.String())
} }
return nil return nil

83
mvc2/ideas/1/main.go Normal file
View File

@ -0,0 +1,83 @@
package main
import (
"fmt"
"github.com/kataras/iris"
"github.com/kataras/iris/sessions"
mvc "github.com/kataras/iris/mvc2"
)
func main() {
app := iris.New()
mvc.New(app.Party("/todo")).Configure(TodoApp)
// no let's have a clear "mvc" package without any conversions and type aliases,
// it's one extra import path for a whole new world, it worths it.
//
// app.UseMVC(app.Party("/todo")).Configure(func(app *iris.MVCApplication))
app.Run(iris.Addr(":8080"))
}
func TodoApp(app *mvc.Application) {
// You can use normal middlewares at MVC apps of course.
app.Router.Use(func(ctx iris.Context) {
ctx.Application().Logger().Infof("Path: %s", ctx.Path())
ctx.Next()
})
// Add dependencies which will be binding to the controller(s),
// can be either a function which accepts an iris.Context and returns a single value (dynamic binding)
// or a static struct value (service).
app.AddDependencies(
mvc.Session(sessions.New(sessions.Config{})),
&prefixedLogger{prefix: "DEV"},
)
app.Register(new(TodoController))
// All dependencies of the parent *mvc.Application
// are cloned to that new child, thefore it has access to the same session as well.
app.NewChild(app.Router.Party("/sub")).
Register(new(TodoSubController))
}
// If controller's fields (or even its functions) expecting an interface
// but a struct value is binded then it will check if that struct value implements
// the interface and if true then it will bind it as expected.
type LoggerService interface {
Log(string)
}
type prefixedLogger struct {
prefix string
}
func (s *prefixedLogger) Log(msg string) {
fmt.Printf("%s: %s\n", s.prefix, msg)
}
type TodoController struct {
Logger LoggerService
Session *sessions.Session
}
func (c *TodoController) Get() string {
count := c.Session.Increment("count", 1)
body := fmt.Sprintf("Hello from TodoController\nTotal visits from you: %d", count)
c.Logger.Log(body)
return body
}
type TodoSubController struct {
Session *sessions.Session
}
func (c *TodoSubController) Get() string {
count, _ := c.Session.GetIntDefault("count", 1)
return fmt.Sprintf("Hello from TodoSubController.\nRead-only visits count: %d", count)
}

90
mvc2/mvc.go Normal file
View File

@ -0,0 +1,90 @@
package mvc2
import "github.com/kataras/iris/core/router"
// Application is the high-level compoment of the "mvc" package.
// It's the API that you will be using to register controllers among wih their
// dependencies that your controllers may expecting.
// It contains the Router(iris.Party) in order to be able to register
// template layout, middleware, done handlers as you used with the
// standard Iris APIBuilder.
//
// The Engine is created by the `New` method and it's the dependencies holder
// and controllers factory.
//
// See `mvc#New` for more.
type Application struct {
Engine *Engine
Router router.Party
}
func newApp(engine *Engine, subRouter router.Party) *Application {
return &Application{
Engine: engine,
Router: subRouter,
}
}
// New returns a new mvc Application based on a "subRouter".
// Application creates a new engine which is responsible for binding the dependencies
// and creating and activating the app's controller(s).
//
// Example: `New(app.Party("/todo"))`.
func New(subRouter router.Party) *Application {
return newApp(NewEngine(), subRouter)
}
// Configure can be used to pass one or more functions that accept this
// Application, use this to add dependencies and controller(s).
//
// Example: `New(app.Party("/todo")).Configure(func(mvcApp *mvc.Application){...})`.
func (app *Application) Configure(configurators ...func(*Application)) *Application {
for _, c := range configurators {
c(app)
}
return app
}
// AddDependencies adds one or more values as dependencies.
// The value can be a single struct value-instance or a function
// which has one input and one output, the input should be
// an `iris.Context` and the output can be any type, that output type
// will be binded to the controller's field, if matching or to the
// controller's methods, if matching.
//
// The dependencies can be changed per-controller as well via a `beforeActivate`
// on the `Register` method or when the controller has the `BeforeActivate(c *ControllerActivator)`
// method defined.
//
// It returns this Application.
//
// Example: `.AddDependencies(loggerService{prefix: "dev"}, func(ctx iris.Context) User {...})`.
func (app *Application) AddDependencies(values ...interface{}) *Application {
app.Engine.Dependencies.Add(values...)
return app
}
// Register adds a controller for the current Router.
// It accept any custom struct which its functions will be transformed
// to routes.
//
// The second, optional and variadic argument is the "beforeActive",
// use that when you want to modify the controller before the activation
// and registration to the main Iris Application.
//
// It returns this Application.
//
// Example: `.Register(new(TodoController))`.
func (app *Application) Register(controller interface{}, beforeActivate ...func(*ControllerActivator)) *Application {
app.Engine.Controller(app.Router, controller, beforeActivate...)
return app
}
// NewChild creates and returns a new Application which will be adapted
// to the "subRouter", it adopts
// the dependencies bindings from the parent(current) one.
//
// Example: `.NewChild(irisApp.Party("/sub")).Register(new(TodoSubController))`.
func (app *Application) NewChild(subRouter router.Party) *Application {
return newApp(app.Engine.Clone(), subRouter)
}

View File

@ -7,7 +7,8 @@ import (
) )
func TestPathParamsBinder(t *testing.T) { func TestPathParamsBinder(t *testing.T) {
m := New().Bind(PathParamsBinder) m := NewEngine()
m.Dependencies.Add(PathParamsBinder)
got := "" got := ""
@ -25,7 +26,8 @@ func TestPathParamsBinder(t *testing.T) {
} }
} }
func TestPathParamBinder(t *testing.T) { func TestPathParamBinder(t *testing.T) {
m := New().Bind(PathParamBinder("username")) m := NewEngine()
m.Dependencies.Add(PathParamBinder("username"))
got := "" got := ""
executed := false executed := false

View File

@ -2,10 +2,8 @@ package mvc2
import ( import (
"reflect" "reflect"
"strings"
"github.com/kataras/iris/context" "github.com/kataras/iris/context"
"github.com/kataras/pkg/zerocheck"
) )
var baseControllerTyp = reflect.TypeOf((*BaseController)(nil)).Elem() var baseControllerTyp = reflect.TypeOf((*BaseController)(nil)).Elem()
@ -20,58 +18,6 @@ func isContext(inTyp reflect.Type) bool {
return inTyp.Implements(contextTyp) return inTyp.Implements(contextTyp)
} }
func indirectVal(v reflect.Value) reflect.Value {
return reflect.Indirect(v)
}
func indirectTyp(typ reflect.Type) reflect.Type {
switch typ.Kind() {
case reflect.Ptr, reflect.Array, reflect.Chan, reflect.Map, reflect.Slice:
return typ.Elem()
}
return typ
}
func goodVal(v reflect.Value) bool {
switch v.Kind() {
case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Interface, reflect.Slice:
if v.IsNil() {
return false
}
}
return v.IsValid()
}
func isFunc(kindable interface {
Kind() reflect.Kind
}) bool {
return kindable.Kind() == reflect.Func
}
func equalTypes(got reflect.Type, expected reflect.Type) bool {
if got == expected {
return true
}
// if accepts an interface, check if the given "got" type does
// implement this "expected" user handler's input argument.
if expected.Kind() == reflect.Interface {
// fmt.Printf("expected interface = %s and got to set on the arg is: %s\n", expected.String(), got.String())
return got.Implements(expected)
}
return false
}
func getNameOf(typ reflect.Type) string {
elemTyp := indirectTyp(typ)
typName := elemTyp.Name()
pkgPath := elemTyp.PkgPath()
fullname := pkgPath[strings.LastIndexByte(pkgPath, '/')+1:] + "." + typName
return fullname
}
func getInputArgsFromFunc(funcTyp reflect.Type) []reflect.Type { func getInputArgsFromFunc(funcTyp reflect.Type) []reflect.Type {
n := funcTyp.NumIn() n := funcTyp.NumIn()
funcIn := make([]reflect.Type, n, n) funcIn := make([]reflect.Type, n, n)
@ -80,72 +26,3 @@ func getInputArgsFromFunc(funcTyp reflect.Type) []reflect.Type {
} }
return funcIn return funcIn
} }
// for controller's fields only.
func structFieldIgnored(f reflect.StructField) bool {
if !f.Anonymous {
return true // if not anonymous(embedded), ignore it.
}
s := f.Tag.Get("ignore")
return s == "true" // if has an ignore tag then ignore it.
}
type field struct {
Type reflect.Type
Index []int // the index of the field, slice if it's part of a embedded struct
Name string // the actual name
// this could be empty, but in our cases it's not,
// it's filled with the bind object (as service which means as static value)
// and it's filled from the lookupFields' caller.
AnyValue reflect.Value
}
func lookupFields(elemTyp reflect.Type, parentIndex []int) (fields []field) {
if elemTyp.Kind() != reflect.Struct {
return
}
for i, n := 0, elemTyp.NumField(); i < n; i++ {
f := elemTyp.Field(i)
if f.PkgPath != "" {
continue // skip unexported.
}
if indirectTyp(f.Type).Kind() == reflect.Struct &&
!structFieldIgnored(f) {
fields = append(fields, lookupFields(f.Type, append(parentIndex, i))...)
continue
}
index := []int{i}
if len(parentIndex) > 0 {
index = append(parentIndex, i)
}
field := field{
Type: f.Type,
Name: f.Name,
Index: index,
}
fields = append(fields, field)
}
return
}
func lookupNonZeroFieldsValues(v reflect.Value) (bindValues []reflect.Value) {
elem := indirectVal(v)
fields := lookupFields(indirectTyp(v.Type()), nil)
for _, f := range fields {
if fieldVal := elem.FieldByIndex(f.Index); f.Type.Kind() == reflect.Ptr && !zerocheck.IsZero(fieldVal) {
bindValues = append(bindValues, fieldVal)
}
}
return
}

View File

@ -17,12 +17,12 @@ type SessionController struct {
Session *sessions.Session Session *sessions.Session
} }
// OnActivate called, once per application lifecycle NOT request, // BeforeActivate called, once per application lifecycle NOT request,
// every single time the dev registers a specific SessionController-based controller. // every single time the dev registers a specific SessionController-based controller.
// It makes sure that its "Manager" field is filled // It makes sure that its "Manager" field is filled
// even if the caller didn't provide any sessions manager via the `app.Controller` function. // even if the caller didn't provide any sessions manager via the `app.Controller` function.
func (s *SessionController) OnActivate(ca *ControllerActivator) { func (s *SessionController) BeforeActivate(ca *ControllerActivator) {
if didntBindManually := ca.Dependencies.BindIfNotExists(defaultSessionManager); didntBindManually { if didntBindManually := ca.Dependencies.AddOnce(defaultSessionManager); didntBindManually {
ca.Router.GetReporter().Add( ca.Router.GetReporter().Add(
`MVC SessionController: couldn't find any "*sessions.Sessions" bindable value to fill the "Manager" field, `MVC SessionController: couldn't find any "*sessions.Sessions" bindable value to fill the "Manager" field,
therefore this controller is using the default sessions manager instead. therefore this controller is using the default sessions manager instead.

View File

@ -167,6 +167,26 @@ func (s *Session) GetIntDefault(key string, defaultValue int) (int, error) {
return defaultValue, errFindParse.Format("int", key, v) return defaultValue, errFindParse.Format("int", key, v)
} }
// Increment increments the stored int value saved as "key" by +"n".
// If value doesn't exist on that "key" then it creates one with the "n" as its value.
// It returns the new, incremented, value.
func (s *Session) Increment(key string, n int) (newValue int) {
newValue, _ = s.GetIntDefault(key, 0)
newValue += n
s.Set(key, newValue)
return
}
// Decrement decrements the stored int value saved as "key" by -"n".
// If value doesn't exist on that "key" then it creates one with the "n" as its value.
// It returns the new, decremented, value even if it's less than zero.
func (s *Session) Decrement(key string, n int) (newValue int) {
newValue, _ = s.GetIntDefault(key, 0)
newValue -= n
s.Set(key, newValue)
return
}
// GetInt64 same as `Get` but returns its int64 representation, // GetInt64 same as `Get` but returns its int64 representation,
// if key doesn't exist then it returns -1. // if key doesn't exist then it returns -1.
func (s *Session) GetInt64(key string) (int64, error) { func (s *Session) GetInt64(key string) (int64, error) {