diff --git a/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go b/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go index 69923ea2..ce2b8869 100644 --- a/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go @@ -10,17 +10,19 @@ import ( // TodoController is our TODO app's web controller. 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 -// things to the controller. -func (c *TodoController) OnActivate(ca *mvc.ControllerActivator) { +// BeforeActivate called once before the server ran, and before +// the routes and dependency binder builded. +// 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 // 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) var ( 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. 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. func (c *TodoController) PutCompleteBy(id int64) int { - item, found := c.service.GetByID(id) + item, found := c.Service.GetByID(id) if !found { return iris.StatusNotFound } - if item.OwnerID != c.session.ID() { + if item.OwnerID != c.Session.ID() { return iris.StatusForbidden } - if !c.service.Complete(item) { + if !c.Service.Complete(item) { return iris.StatusBadRequest } @@ -62,11 +69,11 @@ func (c *TodoController) PutCompleteBy(id int64) int { // Post handles the POST: /todo route. func (c *TodoController) Post(newItem todo.Item) int { - if newItem.OwnerID != c.session.ID() { + if newItem.OwnerID != c.Session.ID() { return iris.StatusForbidden } - if err := c.service.Save(newItem); err != nil { + if err := c.Service.Save(newItem); err != nil { return iris.StatusBadRequest } return iris.StatusOK diff --git a/core/router/api_builder.go b/core/router/api_builder.go index 5ce9150b..7105e9fc 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -10,7 +10,6 @@ import ( "github.com/kataras/iris/context" "github.com/kataras/iris/core/errors" "github.com/kataras/iris/core/router/macro" - "github.com/kataras/iris/mvc/activator" ) const ( @@ -478,85 +477,6 @@ func (api *APIBuilder) Any(relativePath string, handlers ...context.Handler) (ro 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 // which can be changed. var StaticCacheDuration = 20 * time.Second diff --git a/core/router/party.go b/core/router/party.go index 84b17fb0..f1a1a9d0 100644 --- a/core/router/party.go +++ b/core/router/party.go @@ -4,7 +4,6 @@ import ( "github.com/kataras/iris/context" "github.com/kataras/iris/core/errors" "github.com/kataras/iris/core/router/macro" - "github.com/kataras/iris/mvc/activator" ) // Party is here to separate the concept of @@ -130,63 +129,6 @@ type Party interface { // (Get,Post,Put,Head,Patch,Options,Connect,Delete). 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 // to serve all kind of static files. // diff --git a/go19.go b/go19.go index 4df85681..8a16665e 100644 --- a/go19.go +++ b/go19.go @@ -6,7 +6,6 @@ import ( "github.com/kataras/iris/context" "github.com/kataras/iris/core/host" "github.com/kataras/iris/core/router" - "github.com/kataras/iris/mvc" ) type ( @@ -48,132 +47,4 @@ type ( // // A shortcut for the `core/router#Party`, useful when `PartyFunc` is being used. 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 diff --git a/mvc/activator/activate_listener.go b/mvc/activator/activate_listener.go deleted file mode 100644 index 3e2a6120..00000000 --- a/mvc/activator/activate_listener.go +++ /dev/null @@ -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) -} diff --git a/mvc/activator/activator.go b/mvc/activator/activator.go deleted file mode 100644 index 4be839a4..00000000 --- a/mvc/activator/activator.go +++ /dev/null @@ -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 -} diff --git a/mvc/activator/binder.go b/mvc/activator/binder.go deleted file mode 100644 index 81d64dd3..00000000 --- a/mvc/activator/binder.go +++ /dev/null @@ -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) - } -} diff --git a/mvc/activator/field/field.go b/mvc/activator/field/field.go deleted file mode 100644 index 1a3d0413..00000000 --- a/mvc/activator/field/field.go +++ /dev/null @@ -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 -} diff --git a/mvc/activator/methodfunc/func_caller.go b/mvc/activator/methodfunc/func_caller.go deleted file mode 100644 index 108b40c0..00000000 --- a/mvc/activator/methodfunc/func_caller.go +++ /dev/null @@ -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))) - } -} diff --git a/mvc/activator/methodfunc/func_info.go b/mvc/activator/methodfunc/func_info.go deleted file mode 100644 index ca9672cc..00000000 --- a/mvc/activator/methodfunc/func_info.go +++ /dev/null @@ -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 -} diff --git a/mvc/activator/methodfunc/func_lexer.go b/mvc/activator/methodfunc/func_lexer.go deleted file mode 100644 index e06352a7..00000000 --- a/mvc/activator/methodfunc/func_lexer.go +++ /dev/null @@ -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 -} diff --git a/mvc/activator/methodfunc/func_parser.go b/mvc/activator/methodfunc/func_parser.go deleted file mode 100644 index 6ce24e7e..00000000 --- a/mvc/activator/methodfunc/func_parser.go +++ /dev/null @@ -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)) -} diff --git a/mvc/activator/methodfunc/func_result_dispatcher.go b/mvc/activator/methodfunc/func_result_dispatcher.go deleted file mode 100644 index 480a9e36..00000000 --- a/mvc/activator/methodfunc/func_result_dispatcher.go +++ /dev/null @@ -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) -} diff --git a/mvc/activator/methodfunc/methodfunc.go b/mvc/activator/methodfunc/methodfunc.go deleted file mode 100644 index 3a8f6065..00000000 --- a/mvc/activator/methodfunc/methodfunc.go +++ /dev/null @@ -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 -} diff --git a/mvc/activator/model/model.go b/mvc/activator/model/model.go deleted file mode 100644 index 078cc6d4..00000000 --- a/mvc/activator/model/model.go +++ /dev/null @@ -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)) - // } - - } -} diff --git a/mvc/activator/persistence/persistence.go b/mvc/activator/persistence/persistence.go deleted file mode 100644 index 8b81ccbc..00000000 --- a/mvc/activator/persistence/persistence.go +++ /dev/null @@ -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) - } -} diff --git a/mvc/controller.go b/mvc/controller.go deleted file mode 100644 index fe506638..00000000 --- a/mvc/controller.go +++ /dev/null @@ -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) - } -} diff --git a/mvc/controller_test.go b/mvc/controller_test.go deleted file mode 100644 index 362249d5..00000000 --- a/mvc/controller_test.go +++ /dev/null @@ -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") -} diff --git a/mvc/go19.go b/mvc/go19.go deleted file mode 100644 index 7467090f..00000000 --- a/mvc/go19.go +++ /dev/null @@ -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 -) diff --git a/mvc/method_result.go b/mvc/method_result.go deleted file mode 100644 index 7512ff59..00000000 --- a/mvc/method_result.go +++ /dev/null @@ -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 -} diff --git a/mvc/method_result_response.go b/mvc/method_result_response.go deleted file mode 100644 index 4c11cdaa..00000000 --- a/mvc/method_result_response.go +++ /dev/null @@ -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) -} diff --git a/mvc/method_result_test.go b/mvc/method_result_test.go deleted file mode 100644 index b6e2c28e..00000000 --- a/mvc/method_result_test.go +++ /dev/null @@ -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 "text", "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{"text"} -} - -func (c *testControllerMethodResultTypes) GetCustomResponseWithStatusOk() (testControllerMethodCustomResult, int) { - return testControllerMethodCustomResult{"OK"}, iris.StatusOK -} - -func (c *testControllerMethodResultTypes) GetCustomResponseWithStatusNotOk() (testControllerMethodCustomResult, int) { - return testControllerMethodCustomResult{"internal server error"}, 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("text") - - e.GET("/custom/response").Expect().Status(iris.StatusOK). - ContentType("text/html", "utf-8"). - Body().Equal("text") - e.GET("/custom/response/with/status/ok").Expect().Status(iris.StatusOK). - ContentType("text/html", "utf-8"). - Body().Equal("OK") - e.GET("/custom/response/with/status/not/ok").Expect().Status(iris.StatusInternalServerError). - ContentType("text/html", "utf-8"). - Body().Equal("internal server error") - - 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) -} diff --git a/mvc/method_result_view.go b/mvc/method_result_view.go deleted file mode 100644 index 59742fb2..00000000 --- a/mvc/method_result_view.go +++ /dev/null @@ -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) - } -} diff --git a/mvc/session_controller.go b/mvc/session_controller.go deleted file mode 100644 index bc403311..00000000 --- a/mvc/session_controller.go +++ /dev/null @@ -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) -} diff --git a/mvc/strutil.go b/mvc/strutil.go deleted file mode 100644 index 686da607..00000000 --- a/mvc/strutil.go +++ /dev/null @@ -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 -} diff --git a/mvc/strutil_test.go b/mvc/strutil_test.go deleted file mode 100644 index 34648ed4..00000000 --- a/mvc/strutil_test.go +++ /dev/null @@ -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) - } - } - } -} diff --git a/mvc2/bind.go b/mvc2/bind.go index 5cfb8593..63f29cb1 100644 --- a/mvc2/bind.go +++ b/mvc2/bind.go @@ -1,7 +1,7 @@ package mvc2 import ( - "github.com/kataras/di" + "github.com/kataras/iris/mvc2/di" "reflect" ) diff --git a/mvc2/controller.go b/mvc2/controller.go index 4865644c..af17b21e 100644 --- a/mvc2/controller.go +++ b/mvc2/controller.go @@ -3,8 +3,9 @@ package mvc2 import ( "fmt" "reflect" + "strings" - "github.com/kataras/di" + "github.com/kataras/iris/mvc2/di" "github.com/kataras/iris/context" "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. // Its functionality can be overriden by the end-dev. 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 - // 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 // initRef BaseController // the BaseController as it's passed from the end-dev. @@ -88,7 +89,7 @@ type ControllerActivator struct { FullName string // 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 // 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 } +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 { var ( 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, // activate listeners needs them in order to know if something set-ed already or not, // look `BindTypeExists`. - d.Values = append(lookupNonZeroFieldsValues(val), d.Values...) + d.Values = append(di.LookupNonZeroFieldsValues(val), d.Values...) c := &ControllerActivator{ // 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 { - methods := []string{"OnActivate"} + methods := []string{"BeforeActivate"} if isBaseController(typ) { methods = append(methods, "BeginRequest", "EndRequest") } @@ -182,7 +193,6 @@ func (c *ControllerActivator) parseMethods() { } func (c *ControllerActivator) activate() { - c.injector = c.Dependencies.Struct(c.Value) c.parseMethods() } @@ -233,18 +243,40 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware . pathParams := getPathParamsForInput(tmpl.Params, funcIn[1:]...) // get the function's input arguments' bindings. funcDependencies := c.Dependencies.Clone() - funcDependencies.Add(pathParams...) + funcDependencies.AddValue(pathParams...) funcInjector := funcDependencies.Func(m.Func) - // we will make use of 'n' to make a slice of reflect.Value - // to pass into if the function has input arguments that - // are will being filled by the funcDependencies. - 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) + // 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 + // to pass into if the function has input arguments that + // are will being filled by the funcDependencies. + n = len(funcIn) + + elemTyp = di.IndirectType(c.Type) + ) handler := func(ctx context.Context) { ctrl := reflect.New(elemTyp) @@ -263,11 +295,11 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware . defer b.EndRequest(ctx) } - if !c.injector.Valid && !funcInjector.Valid { + if !hasStructInjector && !hasFuncInjector { DispatchFuncResult(ctx, ctrl.Method(m.Index).Call(emptyIn)) } else { ctxValue := reflect.ValueOf(ctx) - if c.injector.Valid { + if hasStructInjector { elem := ctrl.Elem() c.injector.InjectElem(elem, ctxValue) 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... // 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)) } } // otherwise, it has one or more valid input binders, // make the input and call the func using those. - if funcInjector.Valid { + if hasFuncInjector { in := make([]reflect.Value, n, n) in[0] = ctrl funcInjector.Inject(&in, ctxValue) diff --git a/mvc2/controller_handle_test.go b/mvc2/controller_handle_test.go index 26b77f17..4857aeea 100644 --- a/mvc2/controller_handle_test.go +++ b/mvc2/controller_handle_test.go @@ -24,12 +24,11 @@ func (c *testControllerHandle) BeginRequest(ctx iris.Context) { c.reqField = ctx.URLParam("reqfield") } -func (c *testControllerHandle) OnActivate(t *ControllerActivator) { // OnActivate(t *mvc.TController) { - // t.Handle("GET", "/", "Get") - t.Handle("GET", "/histatic", "HiStatic") - t.Handle("GET", "/hiservice", "HiService") - t.Handle("GET", "/hiparam/{ps:string}", "HiParamBy") - t.Handle("GET", "/hiparamempyinput/{ps:string}", "HiParamEmptyInputBy") +func (c *testControllerHandle) BeforeActivate(ca *ControllerActivator) { // BeforeActivate(t *mvc.TController) { + ca.Handle("GET", "/histatic", "HiStatic") + ca.Handle("GET", "/hiservice", "HiService") + ca.Handle("GET", "/hiparam/{ps:string}", "HiParamBy") + ca.Handle("GET", "/hiparamempyinput/{ps:string}", "HiParamEmptyInputBy") } func (c *testControllerHandle) HiStatic() string { @@ -51,8 +50,10 @@ func (c *testControllerHandle) HiParamEmptyInputBy() string { func TestControllerHandle(t *testing.T) { app := iris.New() - m := New() - m.Bind(&TestServiceImpl{prefix: "service:"}).Controller(app, new(testControllerHandle)) + m := NewEngine() + m.Dependencies.Add(&TestServiceImpl{prefix: "service:"}) + m.Controller(app, new(testControllerHandle)) + e := httptest.New(t, app) // test the index, is not part of the current package's implementation but do it. diff --git a/mvc2/controller_test.go b/mvc2/controller_test.go index 785cf133..9ab27476 100644 --- a/mvc2/controller_test.go +++ b/mvc2/controller_test.go @@ -63,7 +63,7 @@ func (c *testControllerAny) Any() { func TestControllerMethodFuncs(t *testing.T) { app := iris.New() - m := New() + m := NewEngine() m.Controller(app, new(testController)) m.Controller(app.Party("/all"), new(testControllerAll)) m.Controller(app.Party("/any"), new(testControllerAny)) @@ -113,7 +113,7 @@ func (c *testControllerBeginAndEndRequestFunc) Post() { func TestControllerBeginAndEndRequestFunc(t *testing.T) { app := iris.New() - New().Controller(app.Party("/profile/{username}"), new(testControllerBeginAndEndRequestFunc)) + NewEngine().Controller(app.Party("/profile/{username}"), new(testControllerBeginAndEndRequestFunc)) e := httptest.New(t, app) usernames := []string{ @@ -156,7 +156,7 @@ func TestControllerBeginAndEndRequestFuncBindMiddleware(t *testing.T) { ctx.Writef("forbidden") } - New().Controller(app.Party("/profile/{username}", middlewareCheck), + NewEngine().Controller(app.Party("/profile/{username}", middlewareCheck), new(testControllerBeginAndEndRequestFunc)) e := httptest.New(t, app) @@ -230,7 +230,7 @@ func (c *testControllerEndRequestAwareness) EndRequest(ctx context.Context) { func TestControllerEndRequestAwareness(t *testing.T) { app := iris.New() - New().Controller(app.Party("/era/{username}"), new(testControllerEndRequestAwareness)) + NewEngine().Controller(app.Party("/era/{username}"), new(testControllerEndRequestAwareness)) e := httptest.New(t, app) usernames := []string{ @@ -284,8 +284,8 @@ func TestControllerBind(t *testing.T) { myTitlePtr := &testBindType{title: t1} // test bind value to value of the correct type myTitleV := testBindType{title: t2} - m := New() - m.Bind(myTitlePtr, myTitleV) + m := NewEngine() + m.Dependencies.Add(myTitlePtr, myTitleV) // or just app m.Controller(app.Party("/"), new(testControllerBindStruct)) m.Controller(app.Party("/deep"), new(testControllerBindDeep)) @@ -345,8 +345,9 @@ func TestControllerInsideControllerRecursively(t *testing.T) { ) app := iris.New() - New().Bind(&testBindType{title: title}). - Controller(app.Party("/user/{username}"), new(testCtrl0)) + m := NewEngine() + m.Dependencies.Add(&testBindType{title: title}) + m.Controller(app.Party("/user/{username}"), new(testCtrl0)) e := httptest.New(t, app) e.GET("/user/" + username).Expect(). @@ -378,7 +379,7 @@ func (c *testControllerRelPathFromFunc) GetSomethingByElseThisBy(bool, int) {} / func TestControllerRelPathFromFunc(t *testing.T) { app := iris.New() - New().Controller(app, new(testControllerRelPathFromFunc)) + NewEngine().Controller(app, new(testControllerRelPathFromFunc)) e := httptest.New(t, app) e.GET("/").Expect().Status(iris.StatusOK). @@ -420,12 +421,8 @@ type testControllerActivateListener struct { TitlePointer *testBindType } -func (c *testControllerActivateListener) OnActivate(ca *ControllerActivator) { - if !ca.Dependencies.BindExists(&testBindType{}) { - ca.Dependencies.Bind(&testBindType{ - title: "default title", - }) - } +func (c *testControllerActivateListener) BeforeActivate(ca *ControllerActivator) { + ca.Dependencies.AddOnce(&testBindType{title: "default title"}) } func (c *testControllerActivateListener) Get() string { @@ -434,12 +431,14 @@ func (c *testControllerActivateListener) Get() string { func TestControllerActivateListener(t *testing.T) { app := iris.New() - New().Controller(app, new(testControllerActivateListener)) - New().Bind(&testBindType{ // will bind to all controllers under this .New() MVC Engine. + NewEngine().Controller(app, new(testControllerActivateListener)) + m := NewEngine() + m.Dependencies.Add(&testBindType{ // will bind to all controllers under this .New() MVC Engine. title: "my title", - }).Controller(app.Party("/manual"), new(testControllerActivateListener)) + }) + m.Controller(app.Party("/manual"), new(testControllerActivateListener)) // or - New().Controller(app.Party("/manual2"), &testControllerActivateListener{ + NewEngine().Controller(app.Party("/manual2"), &testControllerActivateListener{ TitlePointer: &testBindType{ title: "my title", }, diff --git a/mvc2/di/di.go b/mvc2/di/di.go new file mode 100644 index 00000000..d56bed86 --- /dev/null +++ b/mvc2/di/di.go @@ -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..., + ) +} diff --git a/mvc2/di/func.go b/mvc2/di/func.go new file mode 100644 index 00000000..ef9d76ca --- /dev/null +++ b/mvc2/di/func.go @@ -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) +} diff --git a/mvc2/di/object.go b/mvc2/di/object.go new file mode 100644 index 00000000..29de55f5 --- /dev/null +++ b/mvc2/di/object.go @@ -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) +} diff --git a/mvc2/di/reflect.go b/mvc2/di/reflect.go new file mode 100644 index 00000000..0c28bf02 --- /dev/null +++ b/mvc2/di/reflect.go @@ -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 +} diff --git a/mvc2/di/struct.go b/mvc2/di/struct.go new file mode 100644 index 00000000..76e177b0 --- /dev/null +++ b/mvc2/di/struct.go @@ -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 +} diff --git a/mvc2/di/values.go b/mvc2/di/values.go new file mode 100644 index 00000000..129fed0c --- /dev/null +++ b/mvc2/di/values.go @@ -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 +} diff --git a/mvc2/engine.go b/mvc2/engine.go index 59454752..0dc900d6 100644 --- a/mvc2/engine.go +++ b/mvc2/engine.go @@ -1,65 +1,52 @@ package mvc2 import ( - "errors" - - "github.com/kataras/di" + "github.com/kataras/iris/mvc2/di" "github.com/kataras/golog" "github.com/kataras/iris/context" "github.com/kataras/iris/core/router" ) -var ( - errNil = errors.New("nil") - errBad = errors.New("bad") - errAlreadyExists = errors.New("already exists") -) - type Engine struct { - dependencies *di.D + Dependencies *di.D } -func New() *Engine { +func NewEngine() *Engine { return &Engine{ - dependencies: di.New().Hijack(hijacker).GoodFunc(typeChecker), + Dependencies: di.New().Hijack(hijacker).GoodFunc(typeChecker), } } -func (e *Engine) Bind(values ...interface{}) *Engine { - e.dependencies.Bind(values...) - return e -} - -func (e *Engine) Child() *Engine { - child := New() - child.dependencies = e.dependencies.Clone() +func (e *Engine) Clone() *Engine { + child := NewEngine() + child.Dependencies = e.Dependencies.Clone() return child } 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 { golog.Errorf("mvc handler: %v", err) } return h } -func (e *Engine) Controller(router router.Party, controller interface{}, onActivate ...func(*ControllerActivator)) { - ca := newControllerActivator(router, controller, e.dependencies) +func (e *Engine) Controller(router router.Party, controller interface{}, beforeActivate ...func(*ControllerActivator)) { + ca := newControllerActivator(router, controller, e.Dependencies) - // give a priority to the "onActivate" + // give a priority to the "beforeActivate" // callbacks, if any. - for _, cb := range onActivate { + for _, cb := range beforeActivate { cb(ca) } - // check if controller has an "OnActivate" function + // check if controller has an "BeforeActivate" function // which accepts the controller activator and call it. if activateListener, ok := controller.(interface { - OnActivate(*ControllerActivator) + BeforeActivate(*ControllerActivator) }); ok { - activateListener.OnActivate(ca) + activateListener.BeforeActivate(ca) } ca.activate() diff --git a/mvc2/engine_handler_test.go b/mvc2/engine_handler_test.go index 547de0c9..9b4cb417 100644 --- a/mvc2/engine_handler_test.go +++ b/mvc2/engine_handler_test.go @@ -9,7 +9,8 @@ import ( ) func TestMvcEngineInAndHandler(t *testing.T) { - m := New().Bind(testBinderFuncUserStruct, testBinderService, testBinderFuncParam) + m := NewEngine() + m.Dependencies.Add(testBinderFuncUserStruct, testBinderService, testBinderFuncParam) var ( h1 = m.Handler(testConsumeUserHandler) diff --git a/mvc2/func_result.go b/mvc2/func_result.go index d0331949..47a7030d 100644 --- a/mvc2/func_result.go +++ b/mvc2/func_result.go @@ -5,6 +5,8 @@ import ( "strings" "github.com/fatih/structs" + "github.com/kataras/iris/mvc2/di" + "github.com/kataras/iris/context" ) @@ -405,7 +407,7 @@ func (r View) Dispatch(ctx context.Context) { // r as Response view. setViewData(ctx, m) } else if m, ok := r.Data.(context.Map); ok { 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)) } } diff --git a/mvc2/func_result_test.go b/mvc2/func_result_test.go index 240a044b..92be4d55 100644 --- a/mvc2/func_result_test.go +++ b/mvc2/func_result_test.go @@ -71,7 +71,7 @@ func (c *testControllerMethodResult) GetThingWithTryDefaultBy(index int) Result func TestControllerMethodResult(t *testing.T) { app := iris.New() - New().Controller(app, new(testControllerMethodResult)) + NewEngine().Controller(app, new(testControllerMethodResult)) e := httptest.New(t, app) @@ -175,7 +175,7 @@ func (c *testControllerMethodResultTypes) GetCustomStructWithError() (s testCust func TestControllerMethodResultTypes(t *testing.T) { app := iris.New() - New().Controller(app, new(testControllerMethodResultTypes)) + NewEngine().Controller(app, new(testControllerMethodResultTypes)) e := httptest.New(t, app, httptest.LogLevel("debug")) @@ -266,8 +266,8 @@ func (t *testControllerViewResultRespectCtxViewData) Get() Result { func TestControllerViewResultRespectCtxViewData(t *testing.T) { app := iris.New() - New().Controller(app, new(testControllerViewResultRespectCtxViewData), func(ca *ControllerActivator) { - ca.Dependencies.Bind(t) + NewEngine().Controller(app, new(testControllerViewResultRespectCtxViewData), func(ca *ControllerActivator) { + ca.Dependencies.Add(t) }) e := httptest.New(t, app) diff --git a/mvc2/handler.go b/mvc2/handler.go index 99418677..1213732b 100644 --- a/mvc2/handler.go +++ b/mvc2/handler.go @@ -2,7 +2,7 @@ package mvc2 import ( "fmt" - "github.com/kataras/di" + "github.com/kataras/iris/mvc2/di" "reflect" "runtime" @@ -23,7 +23,7 @@ func isContextHandler(handler interface{}) (context.Handler, bool) { } 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 nil diff --git a/mvc2/ideas/1/main.go b/mvc2/ideas/1/main.go new file mode 100644 index 00000000..a63aeda6 --- /dev/null +++ b/mvc2/ideas/1/main.go @@ -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) +} diff --git a/mvc2/mvc.go b/mvc2/mvc.go new file mode 100644 index 00000000..c1a7e04b --- /dev/null +++ b/mvc2/mvc.go @@ -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) +} diff --git a/mvc2/path_param_binder_test.go b/mvc2/path_param_binder_test.go index 582a1ee2..e03555b2 100644 --- a/mvc2/path_param_binder_test.go +++ b/mvc2/path_param_binder_test.go @@ -7,7 +7,8 @@ import ( ) func TestPathParamsBinder(t *testing.T) { - m := New().Bind(PathParamsBinder) + m := NewEngine() + m.Dependencies.Add(PathParamsBinder) got := "" @@ -25,7 +26,8 @@ func TestPathParamsBinder(t *testing.T) { } } func TestPathParamBinder(t *testing.T) { - m := New().Bind(PathParamBinder("username")) + m := NewEngine() + m.Dependencies.Add(PathParamBinder("username")) got := "" executed := false diff --git a/mvc2/reflect.go b/mvc2/reflect.go index 09d4b47d..e18ece73 100644 --- a/mvc2/reflect.go +++ b/mvc2/reflect.go @@ -2,10 +2,8 @@ package mvc2 import ( "reflect" - "strings" "github.com/kataras/iris/context" - "github.com/kataras/pkg/zerocheck" ) var baseControllerTyp = reflect.TypeOf((*BaseController)(nil)).Elem() @@ -20,58 +18,6 @@ func isContext(inTyp reflect.Type) bool { 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 { n := funcTyp.NumIn() funcIn := make([]reflect.Type, n, n) @@ -80,72 +26,3 @@ func getInputArgsFromFunc(funcTyp reflect.Type) []reflect.Type { } 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 -} diff --git a/mvc2/session_binder.go b/mvc2/session.go similarity index 100% rename from mvc2/session_binder.go rename to mvc2/session.go diff --git a/mvc2/session_controller.go b/mvc2/session_controller.go index 8b7e815a..ab6dd394 100644 --- a/mvc2/session_controller.go +++ b/mvc2/session_controller.go @@ -17,12 +17,12 @@ type SessionController struct { 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. // 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(ca *ControllerActivator) { - if didntBindManually := ca.Dependencies.BindIfNotExists(defaultSessionManager); didntBindManually { +func (s *SessionController) BeforeActivate(ca *ControllerActivator) { + if didntBindManually := ca.Dependencies.AddOnce(defaultSessionManager); didntBindManually { ca.Router.GetReporter().Add( `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. diff --git a/sessions/session.go b/sessions/session.go index 3d9d46c8..266f7881 100644 --- a/sessions/session.go +++ b/sessions/session.go @@ -167,6 +167,26 @@ func (s *Session) GetIntDefault(key string, defaultValue int) (int, error) { 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, // if key doesn't exist then it returns -1. func (s *Session) GetInt64(key string) (int64, error) {