1
0
mirror of https://github.com/kataras/iris.git synced 2026-01-09 04:51:56 +00:00

implement a way to add controller functions as handlers with the existing rules respected but it's a bit dirty I will change the implementation and move the mvc2 to mvc and make the api builder's PartyFunc to be a critical part of the controller and the mvc2.Mvc bind values should be also respected to the controller and more

Former-commit-id: e452a916da80d886535b8ae9625d0ba8e2b58d6e
This commit is contained in:
kataras
2017-11-27 21:39:57 +02:00
parent 9d63e3194f
commit dd5de52f34
16 changed files with 496 additions and 206 deletions

View File

@@ -1,22 +1,13 @@
package activator
import (
"reflect"
)
// CallOnActivate simply calls the "controller"'s `OnActivate(*ActivatePayload)` function,
// CallOnActivate simply calls the "controller"'s `OnActivate(*TController)` function,
// if any.
//
// Look `activator.go#Register` and `ActivateListener` for more.
func CallOnActivate(controller interface{},
bindValues *[]interface{}, registerFunc RegisterFunc) {
func CallOnActivate(controller interface{}, tController *TController) {
if ac, ok := controller.(ActivateListener); ok {
p := &ActivatePayload{
BindValues: bindValues,
Handle: registerFunc,
}
ac.OnActivate(p)
ac.OnActivate(tController)
}
}
@@ -27,52 +18,13 @@ func CallOnActivate(controller interface{},
// 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 `ActivatePayload`.
// 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 `ActivatePayload` for more.
OnActivate(*ActivatePayload)
}
// ActivatePayload contains the necessary information and the ability
// to alt a controller's registration options, i.e the binder.
//
// With `ActivatePayload` the `Controller` can register custom routes
// or modify the provided values that will be binded to the
// controller later on.
type ActivatePayload struct {
BindValues *[]interface{}
Handle RegisterFunc
}
// EnsureBindValue will make sure that this "bindValue"
// will be registered to the controller's binder
// if its type is not already passed by the caller..
//
// For example, on `SessionController` it looks if *sessions.Sessions
// has been binded from the caller and if not then the "bindValue"
// will be binded and used as a default sessions manager instead.
//
// At general, if the caller has already provided a value with the same Type
// then the "bindValue" will be ignored and not be added to the controller's bind values.
//
// Returns true if the caller has NOT already provided a value with the same Type
// and "bindValue" is NOT ignored therefore is appended to the controller's bind values.
func (i *ActivatePayload) EnsureBindValue(bindValue interface{}) bool {
valueTyp := reflect.TypeOf(bindValue)
localBindValues := *i.BindValues
for _, bindedValue := range localBindValues {
// type already exists, remember: binding here is per-type.
if reflect.TypeOf(bindedValue) == valueTyp {
return false
}
}
*i.BindValues = append(localBindValues, bindValue)
return true
// Look `TController` for more.
OnActivate(*TController)
}

View File

@@ -4,6 +4,7 @@ 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"
@@ -32,6 +33,12 @@ type (
// 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
@@ -69,37 +76,33 @@ type BaseController interface {
}
// ActivateController returns a new controller type info description.
func ActivateController(base BaseController, bindValues []interface{}) (TController, error) {
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(reflect.ValueOf(base))
val := reflect.Indirect(valPointer)
ctrlName := val.Type().Name()
pkgPath := val.Type().PkgPath()
fullName := pkgPath[strings.LastIndexByte(pkgPath, '/')+1:] + "." + ctrlName
// set the binder, can be nil this check at made at runtime.
binder := newBinder(typ.Elem(), bindValues)
if binder != nil {
for _, bf := range binder.fields {
golog.Debugf("MVC %s: binder loaded for '%s' with value:\n%#v",
fullName, bf.GetFullName(), bf.GetValue())
}
}
t := TController{
t := &TController{
Name: ctrlName,
FullName: fullName,
Type: typ,
Value: val,
binder: binder,
valuePtr: valPointer,
Router: router,
binder: &binder{elemType: typ.Elem()},
modelController: model.Load(typ),
persistenceController: persistence.Load(typ, val),
}
@@ -107,12 +110,35 @@ func ActivateController(base BaseController, bindValues []interface{}) (TControl
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 {
func (t *TController) HandlerOf(methodFunc methodfunc.MethodFunc) context.Handler {
var (
// shared, per-controller
elem = t.Type.Elem()
ctrlName = t.Name
elem = t.Type.Elem()
ctrlName = t.Name
hasBinder = !t.binder.isEmpty()
hasPersistenceData = t.persistenceController != nil
hasModels = t.modelController != nil
@@ -123,7 +149,7 @@ func (t TController) HandlerOf(methodFunc methodfunc.MethodFunc) context.Handler
return func(ctx context.Context) {
// create a new controller instance of that type(>ptr).
c := reflect.New(elem)
if t.binder != nil {
if hasBinder {
t.binder.handle(c)
}
@@ -163,29 +189,38 @@ func (t TController) HandlerOf(methodFunc methodfunc.MethodFunc) context.Handler
}
}
// RegisterFunc used by the caller to register the result routes.
type RegisterFunc func(relPath string, httpMethod string, handler ...context.Handler)
// RegisterMethodHandlers receives a `TController`, description of the
// user's controller, and calls the "registerFunc" for each of its
// method handlers.
//
// Not useful for the end-developer, but may needed for debugging
// at the future.
func RegisterMethodHandlers(t TController, registerFunc RegisterFunc) {
func (t *TController) registerMethodFunc(m methodfunc.MethodFunc) {
var middleware context.Handlers
if t.binder != nil {
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())
// don't stop here.
return
}
// range over the type info's method funcs,
// build a new handler for each of these
@@ -194,35 +229,118 @@ func RegisterMethodHandlers(t TController, registerFunc RegisterFunc) {
// responsible to convert these into routes
// and add them to router via the APIBuilder.
for _, m := range methods {
h := t.HandlerOf(m)
if h == nil {
golog.Warnf("MVC %s: nil method handler found for %s", t.FullName, m.Name)
continue
}
registeredHandlers := append(middleware, h)
registerFunc(m.RelPath, m.HTTPMethod, registeredHandlers...)
golog.Debugf("MVC %s: %s %s maps to function[%d] '%s'", t.FullName,
m.HTTPMethod,
m.RelPath,
m.Index,
m.Name)
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 {
CallOnActivate(controller, &bindValues, registerFunc)
t, err := ActivateController(controller, bindValues)
t, err := newController(controller, registerFunc)
if err != nil {
return err
}
RegisterMethodHandlers(t, registerFunc)
t.BindValue(bindValues...)
CallOnActivate(controller, t)
for _, bf := range t.binder.fields {
golog.Debugf("MVC %s: binder loaded for '%s' with value:\n%#v",
t.FullName, bf.GetFullName(), bf.GetValue())
}
t.resolveAndRegisterMethods()
return nil
}

View File

@@ -8,7 +8,15 @@ import (
"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
@@ -17,28 +25,24 @@ type binder struct {
middleware context.Handlers
}
// 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.
//
// returns a nil binder if values are not valid bindable data to the controller type.
func newBinder(elemType reflect.Type, values []interface{}) *binder {
if len(values) == 0 {
return nil
func (b *binder) bind(value interface{}) {
if value == nil {
return
}
b := &binder{values: values}
b.fields = b.lookup(elemType)
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 nil
return true
}
return b
return false
}
func (b *binder) storeValueIfMiddleware(value reflect.Value) bool {
@@ -55,41 +59,38 @@ func (b *binder) storeValueIfMiddleware(value reflect.Value) bool {
return false
}
func (b *binder) lookup(elem reflect.Type) (fields []field.Field) {
for _, v := range b.values {
value := reflect.ValueOf(v)
// handlers will be recognised as middleware, not struct fields.
// 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.
continue
}
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
}
fields = append(fields, field.LookupFields(elem, matcher, handler)...)
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
}
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) {

View File

@@ -53,38 +53,49 @@ func fetchInfos(typ reflect.Type) (methods []FuncInfo) {
// and add that.
for i, n := 0, typ.NumMethod(); i < n; i++ {
m := typ.Method(i)
name := m.Name
for _, method := range availableMethods {
possibleMethodFuncName := methodTitle(method)
if strings.Index(name, possibleMethodFuncName) == 0 {
trailing := ""
// if has chars after the method itself
if lname, lmethod := len(name), len(possibleMethodFuncName); lname > lmethod {
ch := rune(name[lmethod])
// if the next char is upper, otherise just skip the whole func info.
if unicode.IsUpper(ch) {
trailing = name[lmethod:]
} else {
continue
}
}
methodInfo := FuncInfo{
Name: name,
Trailing: trailing,
Type: m.Type,
HTTPMethod: method,
Index: m.Index,
}
methods = append(methods, methodInfo)
}
if method, ok := FetchFuncInfo(m); ok {
methods = append(methods, method)
}
}
return
}
// FetchFuncInfo returns a FuncInfo based on the method of the controller.
func FetchFuncInfo(m reflect.Method) (FuncInfo, bool) {
name := m.Name
for _, method := range availableMethods {
possibleMethodFuncName := methodTitle(method)
if strings.Index(name, possibleMethodFuncName) == 0 {
trailing := ""
// if has chars after the method itself
if lname, lmethod := len(name), len(possibleMethodFuncName); lname > lmethod {
ch := rune(name[lmethod])
// if the next char is upper, otherise just skip the whole func info.
if unicode.IsUpper(ch) {
trailing = name[lmethod:]
} else {
continue
}
}
info := FuncInfo{
Name: name,
Trailing: trailing,
Type: m.Type,
HTTPMethod: method,
Index: m.Index,
}
return info, true
}
}
return FuncInfo{}, false
}
func methodTitle(httpMethod string) string {
httpMethodFuncName := strings.Title(strings.ToLower(httpMethod))
return httpMethodFuncName

View File

@@ -182,6 +182,7 @@ func (a *ast) paramValues(ctx context.Context) []reflect.Value {
l := len(a.paramKeys)
values := make([]reflect.Value, l, l)
for i := 0; i < l; i++ {
paramKey := a.paramKeys[i]
paramType := a.paramTypes[i]

View File

@@ -31,20 +31,38 @@ func Resolve(typ reflect.Type) ([]MethodFunc, error) {
var methodFuncs []MethodFunc
infos := fetchInfos(typ)
for _, info := range infos {
parser := newFuncParser(info)
a, err := parser.parse()
methodFunc, err := ResolveMethodFunc(info)
if r.AddErr(err) {
continue
}
methodFunc := MethodFunc{
RelPath: a.relPath,
FuncInfo: info,
MethodCall: buildMethodCall(a),
}
methodFuncs = append(methodFuncs, methodFunc)
}
return methodFuncs, r.Return()
}
// ResolveMethodFunc resolves a single `MethodFunc` from a single `FuncInfo`.
func ResolveMethodFunc(info FuncInfo, paramKeys ...string) (MethodFunc, error) {
parser := newFuncParser(info)
a, err := parser.parse()
if err != nil {
return MethodFunc{}, err
}
if len(paramKeys) > 0 {
a.paramKeys = paramKeys
}
methodFunc := MethodFunc{
RelPath: a.relPath,
FuncInfo: info,
MethodCall: buildMethodCall(a),
}
/* TODO: split the method path and ast param keys, and all that
because now we want to use custom param keys but 'paramfirst' is set-ed.
*/
return methodFunc, nil
}

View File

@@ -306,6 +306,7 @@ func (t *testControllerBindDeep) Get() {
}
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
@@ -505,8 +506,8 @@ type testControllerActivateListener struct {
TitlePointer *testBindType
}
func (c *testControllerActivateListener) OnActivate(p *activator.ActivatePayload) {
p.EnsureBindValue(&testBindType{
func (c *testControllerActivateListener) OnActivate(t *activator.TController) {
t.BindValue(&testBindType{
title: "default title",
})
}

View File

@@ -13,15 +13,14 @@ type (
// in order to be marked as safe content, to be rendered as html and not escaped.
HTML = template.HTML
// ActivatePayload contains the necessary information and the ability
// to alt a controller's registration options, i.e the binder.
// TController contains the necessary controller's pre-serve information.
//
// With `ActivatePayload` the `Controller` can register custom routes
// 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#ActivatePayload` for its implementation.
// Look the `mvc/activator#TController` for its implementation.
//
// A shortcut for the `mvc/activator#ActivatePayload`, useful when `OnActivate` is being used.
ActivatePayload = activator.ActivatePayload
// A shortcut for the `mvc/activator#TController`, useful when `OnActivate` is being used.
TController = activator.TController
)

View File

@@ -24,8 +24,9 @@ type SessionController struct {
// 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(p *activator.ActivatePayload) {
if p.EnsureBindValue(defaultManager) {
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`)