1
0
mirror of https://github.com/kataras/iris.git synced 2025-12-17 09:57:01 +00:00

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

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

View File

@@ -2,6 +2,11 @@ package activator
import (
"reflect"
"strings"
"github.com/kataras/iris/mvc/activator/methodfunc"
"github.com/kataras/iris/mvc/activator/model"
"github.com/kataras/iris/mvc/activator/persistence"
"github.com/kataras/golog"
@@ -16,93 +21,23 @@ type (
// think it as a "supervisor" of your Controller which
// cares about you.
TController struct {
// The name of the front controller struct.
Name string
// FullName it's the last package path segment + "." + the Name.
// i.e: if login-example/user/controller.go, the FullName is "user.Controller".
FullName string
// the type of the user/dev's "c" controller (interface{}).
Type reflect.Type
// it's the first passed value of the controller instance,
// we need this to collect and save the persistence fields' values.
Value reflect.Value
binder *binder // executed even before the BeginRequest if not nil.
controls []TControl // executed on request, after the BeginRequest and before the EndRequest.
// the actual method functions
// i.e for "GET" it's the `Get()`
//
// Here we have a strange relation by-design.
// It contains the methods
// but we have different handlers
// for each of these methods,
// while in the same time all of these
// are depend from this TypeInfo struct.
// So we have TypeInfo -> Methods -> Each(TypeInfo, Method.Index)
// -> Handler for X HTTPMethod, see `Register`.
Methods []MethodFunc
}
// MethodFunc is part of the `TController`,
// it contains the index for a specific http method,
// taken from user's controller struct.
MethodFunc struct {
Index int
HTTPMethod string
binder *binder // executed even before the BeginRequest if not nil.
modelController *model.Controller
persistenceController *persistence.Controller
}
)
// ErrControlSkip never shows up, used to determinate
// if a control's Load return error is critical or not,
// `ErrControlSkip` means that activation can continue
// and skip this control.
var ErrControlSkip = errors.New("skip control")
// TControl is an optional feature that an app can benefit
// by using its own custom controls to control the flow
// inside a controller, they are being registered per controller.
//
// Naming:
// I could find better name such as 'Control',
// but I can imagine the user's confusion about `Controller`
// and `Control` types, they are different but they may
// use that as embedded, so it can not start with the world "C..".
// The best name that shows the relation between this
// and the controller type info struct(TController) is the "TControl",
// `TController` is prepended with "T" for the same reasons, it's different
// than `Controller`, the TController is the "description" of the user's
// `Controller` embedded field.
type TControl interface { // or CoreControl?
// Load should returns nil if its `Handle`
// should be called on serve time.
//
// if error is filled then controller info
// is not created and that error is returned to the
// high-level caller, but the `ErrControlSkip` can be used
// to skip the control without breaking the rest of the registration.
Load(t *TController) error
// Handle executes the control.
// It accepts the context, the new controller instance
// and the specific methodFunc based on the request.
Handle(ctx context.Context, controller reflect.Value, methodFunc func())
}
func isControlErr(err error) bool {
if err != nil {
if isSkipper(err) {
return false
}
return true
}
return false
}
func isSkipper(err error) bool {
if err != nil {
if err.Error() == ErrControlSkip.Error() {
return true
}
}
return false
}
// the parent package should complete this "interface"
// it's not exported, so their functions
// but reflect doesn't care about it, so we are ok
@@ -129,10 +64,7 @@ type BaseController interface {
}
// ActivateController returns a new controller type info description.
// A TController is not useful for the end-developer
// but it can be used for debugging.
func ActivateController(base BaseController, bindValues []interface{},
controls []TControl) (TController, error) {
func ActivateController(base BaseController, bindValues []interface{}) (TController, error) {
// get and save the type.
typ := reflect.TypeOf(base)
@@ -146,129 +78,73 @@ func ActivateController(base BaseController, bindValues []interface{},
// values later on.
val := reflect.Indirect(reflect.ValueOf(base))
ctrlName := val.Type().Name()
pkgPath := val.Type().PkgPath()
fullName := pkgPath[strings.LastIndexByte(pkgPath, '/')+1:] + "." + ctrlName
// set the binder, can be nil this check at made at runtime.
binder := newBinder(typ.Elem(), bindValues)
if binder != nil {
for _, bf := range binder.fields {
golog.Debugf("MVC %s: binder loaded for '%s' with value:\n%#v",
ctrlName, bf.getFullName(), bf.getValue())
fullName, bf.GetFullName(), bf.GetValue())
}
}
t := TController{
Type: typ,
Value: val,
binder: binder,
}
// first the custom controls,
// after these, the persistence,
// the method control
// which can set the model and
// last the model control.
controls = append(controls, []TControl{
// PersistenceDataControl stores the optional data
// that will be shared among all requests.
PersistenceDataControl(),
// MethodControl is the actual method function
// i.e for "GET" it's the `Get()` that will be
// fired.
MethodControl(),
// ModelControl stores the optional models from
// the struct's fields values that
// are being setted by the method function
// and set them as ViewData.
ModelControl()}...)
for _, control := range controls {
err := control.Load(&t)
// fail on first control error if not ErrControlSkip.
if isControlErr(err) {
return t, err
}
if isSkipper(err) {
continue
}
golog.Debugf("MVC %s: succeed load of the %#v", ctrlName, control)
t.controls = append(t.controls, control)
Name: ctrlName,
FullName: fullName,
Type: typ,
Value: val,
binder: binder,
modelController: model.Load(typ),
persistenceController: persistence.Load(typ, val),
}
return t, nil
}
// builds the handler for a type based on the method index (i.e Get() -> [0], Post() -> [1]).
func buildMethodHandler(t TController, methodFuncIndex int) context.Handler {
elem := t.Type.Elem()
ctrlName := t.Value.Type().Name()
/*
// good idea, it speeds up the whole thing by ~1MB per 20MB at my personal
// laptop but this way the Model for example which is not a persistence
// variable can stay for the next request
// (if pointer receiver but if not then variables like `Tmpl` cannot stay)
// and that will have unexpected results.
// however we keep it here I want to see it every day in order to find a better way.
// HandlerOf builds the handler for a type based on the specific method func.
func (t TController) HandlerOf(methodFunc methodfunc.MethodFunc) context.Handler {
var (
// shared, per-controller
elem = t.Type.Elem()
ctrlName = t.Name
type runtimeC struct {
method func()
c reflect.Value
elem reflect.Value
b BaseController
}
pool := sync.Pool{
New: func() interface{} {
c := reflect.New(elem)
methodFunc := c.Method(methodFuncIndex).Interface().(func())
b, _ := c.Interface().(BaseController)
elem := c.Elem()
if t.binder != nil {
t.binder.handle(elem)
}
rc := runtimeC{
c: c,
elem: elem,
b: b,
method: methodFunc,
}
return rc
},
}
*/
hasPersistenceData = t.persistenceController != nil
hasModels = t.modelController != nil
// per-handler
handleRequest = methodFunc.MethodCall
)
return func(ctx context.Context) {
// // create a new controller instance of that type(>ptr).
// create a new controller instance of that type(>ptr).
c := reflect.New(elem)
if t.binder != nil {
t.binder.handle(c)
if ctx.IsStopped() {
return
}
}
// get the Controller embedded field's addr.
// it should never be invalid here because we made that checks on activation.
// but if somone tries to "crack" that, then just stop the world in order to be notified,
// we don't want to go away from that type of mistake.
b := c.Interface().(BaseController)
b.SetName(ctrlName)
// if has persistence data then set them
// before the end-developer's handler in order to be available there.
if hasPersistenceData {
t.persistenceController.Handle(c)
}
// init the request.
b.BeginRequest(ctx)
if ctx.IsStopped() {
return
}
methodFunc := c.Method(methodFuncIndex).Interface().(func())
// execute the controls by order, including the method control.
for _, control := range t.controls {
if ctx.IsStopped() {
break
}
control.Handle(ctx, c, methodFunc)
// the most important, execute the specific function
// from the controller that is responsible to handle
// this request, by method and path.
handleRequest(ctx, c.Method(methodFunc.Index).Interface())
// if had models, set them after the end-developer's handler.
if hasModels {
t.modelController.Handle(ctx, c)
}
// finally, execute the controller, don't check for IsStopped.
@@ -277,7 +153,7 @@ func buildMethodHandler(t TController, methodFuncIndex int) context.Handler {
}
// RegisterFunc used by the caller to register the result routes.
type RegisterFunc func(httpMethod string, handler ...context.Handler)
type RegisterFunc func(relPath string, httpMethod string, handler ...context.Handler)
// RegisterMethodHandlers receives a `TController`, description of the
// user's controller, and calls the "registerFunc" for each of its
@@ -286,37 +162,47 @@ type RegisterFunc func(httpMethod string, handler ...context.Handler)
// Not useful for the end-developer, but may needed for debugging
// at the future.
func RegisterMethodHandlers(t TController, registerFunc RegisterFunc) {
var middleware context.Handlers
if t.binder != nil {
if m := t.binder.middleware; len(m) > 0 {
middleware = m
}
}
// the actual method functions
// i.e for "GET" it's the `Get()`.
methods := methodfunc.Resolve(t.Type)
// range over the type info's method funcs,
// build a new handler for each of these
// methods and register them to their
// http methods using the registerFunc, which is
// responsible to convert these into routes
// and add them to router via the APIBuilder.
var handlers context.Handlers
if t.binder != nil {
if m := t.binder.middleware; len(m) > 0 {
handlers = append(handlers, t.binder.middleware...)
for _, m := range methods {
h := t.HandlerOf(m)
if h == nil {
golog.Debugf("MVC %s: nil method handler found for %s", t.FullName, m.Name)
continue
}
}
registeredHandlers := append(middleware, h)
registerFunc(m.RelPath, m.HTTPMethod, registeredHandlers...)
for _, m := range t.Methods {
methodHandler := buildMethodHandler(t, m.Index)
registeredHandlers := append(handlers, methodHandler)
registerFunc(m.HTTPMethod, registeredHandlers...)
golog.Debugf("MVC %s: %s %s maps to function[%d] '%s'", t.FullName,
m.HTTPMethod,
m.RelPath,
m.Index,
m.Name)
}
}
// Register receives a "controller",
// a pointer of an instance which embeds the `Controller`,
// the value of "baseControllerFieldName" should be `Controller`
// if embedded and "controls" that can intercept on controller
// activation and on the controller's handler, at serve-time.
func Register(controller BaseController, bindValues []interface{}, controls []TControl,
// the value of "baseControllerFieldName" should be `Controller`.
func Register(controller BaseController, bindValues []interface{},
registerFunc RegisterFunc) error {
t, err := ActivateController(controller, bindValues, controls)
t, err := ActivateController(controller, bindValues)
if err != nil {
return err
}

View File

@@ -3,12 +3,14 @@ package activator
import (
"reflect"
"github.com/kataras/iris/mvc/activator/field"
"github.com/kataras/iris/context"
)
type binder struct {
values []interface{}
fields []field
fields []field.Field
// saves any middleware that may need to be passed to the router,
// statically, to gain performance.
@@ -53,7 +55,7 @@ func (b *binder) storeValueIfMiddleware(value reflect.Value) bool {
return false
}
func (b *binder) lookup(elem reflect.Type) (fields []field) {
func (b *binder) lookup(elem reflect.Type) (fields []field.Field) {
for _, v := range b.values {
value := reflect.ValueOf(v)
// handlers will be recognised as middleware, not struct fields.
@@ -70,11 +72,11 @@ func (b *binder) lookup(elem reflect.Type) (fields []field) {
return elemField.Type == value.Type()
}
handler := func(f *field) {
handler := func(f *field.Field) {
f.Value = value
}
fields = append(fields, lookupFields(elem, matcher, handler)...)
fields = append(fields, field.LookupFields(elem, matcher, handler)...)
}
return
}
@@ -89,6 +91,6 @@ func (b *binder) handle(c reflect.Value) {
elem := c.Elem() // controller should always be a pointer at this state
for _, f := range b.fields {
f.sendTo(elem)
f.SendTo(elem)
}
}

View File

@@ -1,53 +0,0 @@
package activator
import (
"reflect"
"github.com/kataras/iris/context"
)
func getCustomFuncIndex(t *TController, funcNames ...string) (funcIndex int, has bool) {
val := t.Value
for _, funcName := range funcNames {
if m, has := t.Type.MethodByName(funcName); has {
if _, isRequestFunc := val.Method(m.Index).Interface().(func(ctx context.Context)); isRequestFunc {
return m.Index, has
}
}
}
return -1, false
}
type callableControl struct {
Functions []string
index int
}
func (cc *callableControl) Load(t *TController) error {
funcIndex, has := getCustomFuncIndex(t, cc.Functions...)
if !has {
return ErrControlSkip
}
cc.index = funcIndex
return nil
}
// the "c" is a new "c" instance
// which is being used at serve time, inside the Handler.
// it calls the custom function (can be "Init", "BeginRequest", "End" and "EndRequest"),
// the check of this function made at build time, so it's a safe a call.
func (cc *callableControl) Handle(ctx context.Context, c reflect.Value, methodFunc func()) {
c.Method(cc.index).Interface().(func(ctx context.Context))(ctx)
}
// CallableControl is a generic-propose `TControl`
// which finds one function in the user's controller's struct
// based on the possible "funcName(s)" and executes
// that inside the handler, at serve-time, by passing
// the current request's `iris/context/#Context`.
func CallableControl(funcName ...string) TControl {
return &callableControl{Functions: funcName}
}

View File

@@ -1,10 +1,13 @@
package activator
package field
import (
"reflect"
)
type field struct {
// Field is a controller's field
// contains all the necessary, internal, information
// to work with.
type Field struct {
Name string // the field's original name
// but if a tag with `name: "other"`
// exist then this fill is filled, otherwise it's the same as the Name.
@@ -13,55 +16,55 @@ type field struct {
Type reflect.Type
Value reflect.Value
embedded *field
embedded *Field
}
// getIndex returns all the "dimensions"
// GetIndex returns all the "dimensions"
// of the controller struct field's position that this field is referring to,
// recursively.
// Usage: elem.FieldByIndex(field.getIndex())
// for example the {0,1} means that the field is on the second field of the first's
// field of this struct.
func (ff field) getIndex() []int {
func (ff Field) GetIndex() []int {
deepIndex := []int{ff.Index}
if emb := ff.embedded; emb != nil {
deepIndex = append(deepIndex, emb.getIndex()...)
deepIndex = append(deepIndex, emb.GetIndex()...)
}
return deepIndex
}
// getType returns the type of the referring field, recursively.
func (ff field) getType() reflect.Type {
// GetType returns the type of the referring field, recursively.
func (ff Field) GetType() reflect.Type {
typ := ff.Type
if emb := ff.embedded; emb != nil {
return emb.getType()
return emb.GetType()
}
return typ
}
// getFullName returns the full name of that field
// GetFullName returns the full name of that field
// i.e: UserController.SessionController.Manager,
// it's useful for debugging only.
func (ff field) getFullName() string {
func (ff Field) GetFullName() string {
name := ff.Name
if emb := ff.embedded; emb != nil {
return name + "." + emb.getFullName()
return name + "." + emb.GetFullName()
}
return name
}
// getTagName returns the tag name of the referring field
// GetTagName returns the tag name of the referring field
// recursively.
func (ff field) getTagName() string {
func (ff Field) GetTagName() string {
name := ff.TagName
if emb := ff.embedded; emb != nil {
return emb.getTagName()
return emb.GetTagName()
}
return name
@@ -74,10 +77,10 @@ func checkVal(val reflect.Value) bool {
return val.IsValid() && (val.Kind() == reflect.Ptr && !val.IsNil()) && val.CanInterface()
}
// getValue returns a valid value of the referring field, recursively.
func (ff field) getValue() interface{} {
// GetValue returns a valid value of the referring field, recursively.
func (ff Field) GetValue() interface{} {
if ff.embedded != nil {
return ff.embedded.getValue()
return ff.embedded.GetValue()
}
if checkVal(ff.Value) {
@@ -87,17 +90,17 @@ func (ff field) getValue() interface{} {
return "undefinied value"
}
// sendTo should be used when this field or its embedded
// SendTo should be used when this field or its embedded
// has a Value on it.
// It sets the field's value to the "elem" instance, it's the new controller.
func (ff field) sendTo(elem reflect.Value) {
func (ff Field) SendTo(elem reflect.Value) {
// note:
// we don't use the getters here
// because we do recursively search by our own here
// to be easier to debug if ever needed.
if embedded := ff.embedded; embedded != nil {
if ff.Index >= 0 {
embedded.sendTo(elem.Field(ff.Index))
embedded.SendTo(elem.Field(ff.Index))
}
return
}
@@ -121,18 +124,18 @@ func lookupTagName(elemField reflect.StructField) string {
return vname
}
// lookupFields iterates all "elem"'s fields and its fields
// LookupFields iterates all "elem"'s fields and its fields
// if structs, recursively.
// Compares them to the "matcher", if they passed
// then it executes the "handler" if any,
// the handler can change the field as it wants to.
//
// It finally returns that collection of the valid fields, can be empty.
func lookupFields(elem reflect.Type, matcher func(reflect.StructField) bool, handler func(*field)) (fields []field) {
func LookupFields(elem reflect.Type, matcher func(reflect.StructField) bool, handler func(*Field)) (fields []Field) {
for i, n := 0, elem.NumField(); i < n; i++ {
elemField := elem.Field(i)
if matcher(elemField) {
field := field{
field := Field{
Index: i,
Name: elemField.Name,
TagName: lookupTagName(elemField),
@@ -150,7 +153,7 @@ func lookupFields(elem reflect.Type, matcher func(reflect.StructField) bool, han
f := lookupStructField(elemField.Type, matcher, handler)
if f != nil {
fields = append(fields, field{
fields = append(fields, Field{
Index: i,
Name: elemField.Name,
Type: elemField.Type,
@@ -168,7 +171,7 @@ func lookupFields(elem reflect.Type, matcher func(reflect.StructField) bool, han
// for both structured (embedded) fields and normal fields
// but we keep that as it's, a new function like this
// is easier for debugging, if ever needed.
func lookupStructField(elem reflect.Type, matcher func(reflect.StructField) bool, handler func(*field)) *field {
func lookupStructField(elem reflect.Type, matcher func(reflect.StructField) bool, handler func(*Field)) *Field {
// fmt.Printf("lookup struct for elem: %s\n", elem.Name())
// ignore if that field is not a struct
@@ -181,7 +184,7 @@ func lookupStructField(elem reflect.Type, matcher func(reflect.StructField) bool
elemField := elem.Field(i)
if matcher(elemField) {
// we area inside the correct type.
f := &field{
f := &Field{
Index: i,
Name: elemField.Name,
TagName: lookupTagName(elemField),
@@ -202,7 +205,7 @@ func lookupStructField(elem reflect.Type, matcher func(reflect.StructField) bool
elemFieldEmb := elem.Field(i)
f := lookupStructField(elemFieldEmb.Type, matcher, handler)
if f != nil {
fp := &field{
fp := &Field{
Index: i,
Name: elemFieldEmb.Name,
TagName: lookupTagName(elemFieldEmb),

View File

@@ -1,81 +0,0 @@
package activator
import (
"reflect"
"strings"
"github.com/kataras/iris/context"
"github.com/kataras/iris/core/errors"
)
var availableMethods = [...]string{
"ANY", // will be registered using the `core/router#APIBuilder#Any`
"ALL", // same as ANY
"NONE", // offline route
// valid http methods
"GET",
"POST",
"PUT",
"DELETE",
"CONNECT",
"HEAD",
"PATCH",
"OPTIONS",
"TRACE",
}
type methodControl struct{}
// ErrMissingHTTPMethodFunc fired when the controller doesn't handle any valid HTTP method.
var ErrMissingHTTPMethodFunc = errors.New(`controller can not be activated,
missing a compatible HTTP method function, i.e Get()`)
func (mc *methodControl) Load(t *TController) error {
// search the entire controller
// for any compatible method function
// and register that.
for _, method := range availableMethods {
if m, ok := t.Type.MethodByName(getMethodName(method)); ok {
t.Methods = append(t.Methods, MethodFunc{
HTTPMethod: method,
Index: m.Index,
})
// check if method was Any() or All()
// if yes, then break to skip any conflict with the rest of the method functions.
// (this will be registered to all valid http methods by the APIBuilder)
if method == "ANY" || method == "ALL" {
break
}
}
}
if len(t.Methods) == 0 {
// no compatible method found, fire an error and stop everything.
return ErrMissingHTTPMethodFunc
}
return nil
}
func getMethodName(httpMethod string) string {
httpMethodFuncName := strings.Title(strings.ToLower(httpMethod))
return httpMethodFuncName
}
func (mc *methodControl) Handle(ctx context.Context, c reflect.Value, methodFunc func()) {
// execute the responsible method for that handler.
// Remember:
// To improve the performance
// we don't compare the ctx.Method()[HTTP Method]
// to the instance's Method, each handler is registered
// to a specific http method.
methodFunc()
}
// MethodControl loads and serve the main functionality of the controllers,
// which is to run a function based on the http method (pre-computed).
func MethodControl() TControl {
return &methodControl{}
}

View File

@@ -0,0 +1,61 @@
package methodfunc
import (
"github.com/kataras/iris/context"
)
// FuncCaller is responsible to call the controller's function
// which is responsible
// for that request for this http method.
type FuncCaller interface {
// MethodCall fires the actual handler.
// The "ctx" is the current context, helps us to get any path parameter's values.
//
// The "f" is the controller's function which is responsible
// for that request for this http method.
// That function can accept one parameter.
//
// The default callers (and the only one for now)
// are pre-calculated by the framework.
MethodCall(ctx context.Context, f interface{})
}
type callerFunc func(ctx context.Context, f interface{})
func (c callerFunc) MethodCall(ctx context.Context, f interface{}) {
c(ctx, f)
}
func resolveCaller(p pathInfo) callerFunc {
// if it's standard `Get`, `Post` without parameters.
if p.ParamType == "" {
return func(ctx context.Context, f interface{}) {
f.(func())()
}
}
// remember,
// the router already checks for the correct type,
// we did pre-calculate everything
// and now we will pre-calculate the method caller itself as well.
if p.ParamType == paramTypeInt {
return func(ctx context.Context, f interface{}) {
paramValue, _ := ctx.Params().GetInt(paramName)
f.(func(int))(paramValue)
}
}
if p.ParamType == paramTypeLong {
return func(ctx context.Context, f interface{}) {
paramValue, _ := ctx.Params().GetInt64(paramName)
f.(func(int64))(paramValue)
}
}
// else it's string or path, both of them are simple strings.
return func(ctx context.Context, f interface{}) {
paramValue := ctx.Params().Get(paramName)
f.(func(string))(paramValue)
}
}

View File

@@ -0,0 +1,92 @@
package methodfunc
import (
"reflect"
"strings"
"unicode"
)
var availableMethods = [...]string{
"ANY", // will be registered using the `core/router#APIBuilder#Any`
"ALL", // same as ANY
"NONE", // offline route
// valid http methods
"GET",
"POST",
"PUT",
"DELETE",
"CONNECT",
"HEAD",
"PATCH",
"OPTIONS",
"TRACE",
}
// FuncInfo is part of the `TController`,
// it contains the index for a specific http method,
// taken from user's controller struct.
type FuncInfo struct {
// Name is the map function name.
Name string
// Trailing is not empty when the Name contains
// characters after the titled method, i.e
// if Name = Get -> empty
// if Name = GetLogin -> Login
// if Name = GetUserPost -> UserPost
Trailing string
// The Type of the method, includes the receivers.
Type reflect.Type
// Index is the index of this function inside the controller type.
Index int
// HTTPMethod is the original http method that this
// function should be registered to and serve.
// i.e "GET","POST","PUT"...
HTTPMethod string
}
// or resolve methods
func fetchInfos(typ reflect.Type) (methods []FuncInfo) {
// search the entire controller
// for any compatible method function
// and add that.
for i, n := 0, typ.NumMethod(); i < n; i++ {
m := typ.Method(i)
name := m.Name
for _, method := range availableMethods {
possibleMethodFuncName := methodTitle(method)
if strings.Index(name, possibleMethodFuncName) == 0 {
trailing := ""
// if has chars after the method itself
if lname, lmethod := len(name), len(possibleMethodFuncName); lname > lmethod {
ch := rune(name[lmethod])
// if the next char is upper, otherise just skip the whole func info.
if unicode.IsUpper(ch) {
trailing = name[lmethod:]
} else {
continue
}
}
methodInfo := FuncInfo{
Name: name,
Trailing: trailing,
Type: m.Type,
HTTPMethod: method,
Index: m.Index,
}
methods = append(methods, methodInfo)
}
}
}
return
}
func methodTitle(httpMethod string) string {
httpMethodFuncName := strings.Title(strings.ToLower(httpMethod))
return httpMethodFuncName
}

View File

@@ -0,0 +1,119 @@
package methodfunc
import (
"bytes"
"fmt"
"strings"
"unicode"
)
const (
by = "By"
wildcard = "Wildcard"
paramName = "param"
)
type pathInfo struct {
GoParamType string
ParamType string
RelPath string
}
const (
paramTypeInt = "int"
paramTypeLong = "long"
paramTypeString = "string"
paramTypePath = "path"
)
var macroTypes = map[string]string{
"int": paramTypeInt,
"int64": paramTypeLong,
"string": paramTypeString,
// there is "path" param type but it's being captured "on-air"
// "file" param type is not supported by the current implementation, yet
// but if someone ask for it I'll implement it, it's easy.
}
func resolveRelativePath(info FuncInfo) (p pathInfo, ok bool) {
if info.Trailing == "" {
// it's valid
// it's just don't have a relative path,
// therefore p.RelPath will be empty, as we want.
return p, true
}
var (
typ = info.Type
tr = info.Trailing
relPath = resolvePathFromFunc(tr)
goType, paramType string
)
byKeywordIdx := strings.LastIndex(tr, by)
if byKeywordIdx != -1 && typ.NumIn() == 2 { // first is the struct receiver.
funcPath := tr[0:byKeywordIdx] // remove the "By"
goType = typ.In(1).Name()
afterBy := byKeywordIdx + len(by)
if len(tr) > afterBy {
if tr[afterBy:] == wildcard {
paramType = paramTypePath
} else {
// invalid syntax
return p, false
}
} else {
// it's not wildcard, so check base on our available macro types.
if paramType, ok = macroTypes[goType]; !ok {
// ivalid type
return p, false
}
}
// int and string are supported.
// as there is no way to get the parameter name
// we will use the "param" everywhere.
suffix := fmt.Sprintf("/{%s:%s}", paramName, paramType)
relPath = resolvePathFromFunc(funcPath) + suffix
}
// if GetSomething/PostSomething/PutSomething...
// we will not check for "Something" because we could
// occur unexpected behaviors to the existing users
// who using exported functions for controller's internal
// functionalities and not for serving a request path.
return pathInfo{
GoParamType: goType,
ParamType: paramType,
RelPath: relPath,
}, true
}
func resolvePathFromFunc(funcName string) string {
end := len(funcName)
start := -1
buf := &bytes.Buffer{}
for i, n := 0, end; i < n; i++ {
c := rune(funcName[i])
if unicode.IsUpper(c) {
// it doesn't count the last uppercase
if start != -1 {
end = i
s := "/" + strings.ToLower(funcName[start:end])
buf.WriteString(s)
}
start = i
continue
}
end = i + 1
}
if end > 0 && len(funcName) >= end {
buf.WriteString("/" + strings.ToLower(funcName[start:end]))
}
return buf.String()
}

View File

@@ -0,0 +1,35 @@
package methodfunc
import (
"reflect"
)
// MethodFunc the handler function.
type MethodFunc struct {
FuncInfo
FuncCaller
RelPath string
}
// Resolve returns all the method funcs
// necessary information and actions to
// perform the request.
func Resolve(typ reflect.Type) (methodFuncs []MethodFunc) {
infos := fetchInfos(typ)
for _, info := range infos {
p, ok := resolveRelativePath(info)
if !ok {
continue
}
caller := resolveCaller(p)
methodFunc := MethodFunc{
RelPath: p.RelPath,
FuncInfo: info,
FuncCaller: caller,
}
methodFuncs = append(methodFuncs, methodFunc)
}
return
}

View File

@@ -1,16 +1,27 @@
package activator
package model
import (
"reflect"
"github.com/kataras/iris/mvc/activator/field"
"github.com/kataras/iris/context"
)
type modelControl struct {
fields []field
// Controller is responsible
// to load and handle the `Model(s)` inside a controller struct
// via the `iris:"model"` tag field.
// It stores the optional models from
// the struct's fields values that
// are being setted by the method function
// and set them as ViewData.
type Controller struct {
fields []field.Field
}
func (mc *modelControl) Load(t *TController) error {
// Load tries to lookup and set for any valid model field.
// Returns nil if no models are being used.
func Load(typ reflect.Type) *Controller {
matcher := func(f reflect.StructField) bool {
if tag, ok := f.Tag.Lookup("iris"); ok {
if tag == "model" {
@@ -20,26 +31,27 @@ func (mc *modelControl) Load(t *TController) error {
return false
}
fields := lookupFields(t.Type.Elem(), matcher, nil)
fields := field.LookupFields(typ.Elem(), matcher, nil)
if len(fields) == 0 {
// first is the `Controller` so we need to
// check the second and after that.
return ErrControlSkip
return nil
}
mc.fields = fields
return nil
mc := &Controller{
fields: fields,
}
return mc
}
func (mc *modelControl) Handle(ctx context.Context, c reflect.Value, methodFunc func()) {
// Handle transfer the models to the view.
func (mc *Controller) Handle(ctx context.Context, c reflect.Value) {
elem := c.Elem() // controller should always be a pointer at this state
for _, f := range mc.fields {
index := f.getIndex()
typ := f.getType()
name := f.getTagName()
index := f.GetIndex()
typ := f.GetType()
name := f.GetTagName()
elemField := elem.FieldByIndex(index)
// check if current controller's element field
@@ -52,12 +64,10 @@ func (mc *modelControl) Handle(ctx context.Context, c reflect.Value, methodFunc
fieldValue := elemField.Interface()
ctx.ViewData(name, fieldValue)
// /*maybe some time in the future*/ if resetable {
// // clean up
// elemField.Set(reflect.Zero(typ))
// }
}
}
// ModelControl returns a TControl which is responsible
// to load and handle the `Model(s)` inside a controller struct
// via the `iris:"model"` tag field.
func ModelControl() TControl {
return &modelControl{}
}

View File

@@ -0,0 +1,60 @@
package persistence
import (
"reflect"
"github.com/kataras/iris/mvc/activator/field"
)
// Controller is responsible to load from the original
// end-developer's main controller's value
// and re-store the persistence data by scanning the original.
// It stores and sets to each new controller
// the optional data that should be shared among all requests.
type Controller struct {
fields []field.Field
}
// Load scans and load for persistence data based on the `iris:"persistence"` tag.
//
// The type is the controller's Type.
// the "val" is the original end-developer's controller's Value.
// Returns nil if no persistence data to store found.
func Load(typ reflect.Type, val reflect.Value) *Controller {
matcher := func(elemField reflect.StructField) bool {
if tag, ok := elemField.Tag.Lookup("iris"); ok {
if tag == "persistence" {
return true
}
}
return false
}
handler := func(f *field.Field) {
valF := val.Field(f.Index)
if valF.IsValid() || (valF.Kind() == reflect.Ptr && !valF.IsNil()) {
val := reflect.ValueOf(valF.Interface())
if val.IsValid() || (val.Kind() == reflect.Ptr && !val.IsNil()) {
f.Value = val
}
}
}
fields := field.LookupFields(typ.Elem(), matcher, handler)
if len(fields) == 0 {
return nil
}
return &Controller{
fields: fields,
}
}
// Handle re-stores the persistence data at the current controller.
func (pc *Controller) Handle(c reflect.Value) {
elem := c.Elem() // controller should always be a pointer at this state
for _, f := range pc.fields {
f.SendTo(elem)
}
}

View File

@@ -1,57 +0,0 @@
package activator
import (
"reflect"
"github.com/kataras/iris/context"
)
type persistenceDataControl struct {
fields []field
}
func (d *persistenceDataControl) Load(t *TController) error {
matcher := func(elemField reflect.StructField) bool {
if tag, ok := elemField.Tag.Lookup("iris"); ok {
if tag == "persistence" {
return true
}
}
return false
}
handler := func(f *field) {
valF := t.Value.Field(f.Index)
if valF.IsValid() || (valF.Kind() == reflect.Ptr && !valF.IsNil()) {
val := reflect.ValueOf(valF.Interface())
if val.IsValid() || (val.Kind() == reflect.Ptr && !val.IsNil()) {
f.Value = val
}
}
}
fields := lookupFields(t.Type.Elem(), matcher, handler)
if len(fields) == 0 {
// first is the `Controller` so we need to
// check the second and after that.
return ErrControlSkip
}
d.fields = fields
return nil
}
func (d *persistenceDataControl) Handle(ctx context.Context, c reflect.Value, methodFunc func()) {
elem := c.Elem() // controller should always be a pointer at this state
for _, f := range d.fields {
f.sendTo(elem)
}
}
// PersistenceDataControl loads and re-stores
// the persistence data by scanning the original
// `TController.Value` instance of the user's controller.
func PersistenceDataControl() TControl {
return &persistenceDataControl{}
}

View File

@@ -436,3 +436,41 @@ func TestControllerInsideControllerRecursively(t *testing.T) {
e.GET("/user/" + username).Expect().
Status(httptest.StatusOK).Body().Equal(expected)
}
type testControllerRelPathFromFunc struct{ mvc.Controller }
func (c *testControllerRelPathFromFunc) EndRequest(ctx context.Context) {
ctx.Writef("%s:%s", ctx.Method(), ctx.Path())
c.Controller.EndRequest(ctx)
}
func (c *testControllerRelPathFromFunc) Get() {}
func (c *testControllerRelPathFromFunc) GetLogin() {}
func (c *testControllerRelPathFromFunc) PostLogin() {}
func (c *testControllerRelPathFromFunc) GetAdminLogin() {}
func (c *testControllerRelPathFromFunc) PutSomethingIntoThis() {}
func (c *testControllerRelPathFromFunc) GetBy(int64) {}
func (c *testControllerRelPathFromFunc) GetByWildcard(string) {}
func TestControllerRelPathFromFunc(t *testing.T) {
app := iris.New()
app.Controller("/", new(testControllerRelPathFromFunc))
e := httptest.New(t, app)
e.GET("/").Expect().Status(httptest.StatusOK).
Body().Equal("GET:/")
e.GET("/login").Expect().Status(httptest.StatusOK).
Body().Equal("GET:/login")
e.POST("/login").Expect().Status(httptest.StatusOK).
Body().Equal("POST:/login")
e.GET("/admin/login").Expect().Status(httptest.StatusOK).
Body().Equal("GET:/admin/login")
e.PUT("/something/into/this").Expect().Status(httptest.StatusOK).
Body().Equal("PUT:/something/into/this")
e.GET("/42").Expect().Status(httptest.StatusOK).
Body().Equal("GET:/42")
e.GET("/anything/here").Expect().Status(httptest.StatusOK).
Body().Equal("GET:/anything/here")
}