1
0
mirror of https://github.com/kataras/iris.git synced 2025-12-21 20:07:04 +00:00

Update to 8.3.0 | MVC Models and Bindings and fix of #723 , read HISTORY.md

Former-commit-id: d8f66d8d370c583a288333df2a14c6ee2dc56466
This commit is contained in:
kataras
2017-08-18 17:09:18 +03:00
parent 398d1e816c
commit b96476d100
53 changed files with 12642 additions and 1046 deletions

312
mvc/activator/activator.go Normal file
View File

@@ -0,0 +1,312 @@
package activator
import (
"reflect"
"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 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
}
)
// 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
// 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`.
ErrMissingControllerInstance = errors.New("controller should have a field of Controller 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.
type BaseController interface {
BeginRequest(ctx context.Context)
EndRequest(ctx context.Context)
}
// 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) {
// get and save the type.
typ := reflect.TypeOf(base)
if typ.Kind() != reflect.Ptr {
typ = reflect.PtrTo(typ)
}
// 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))
ctrlName := val.Type().Name()
// 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 field index of: %d",
ctrlName, bf.Name, bf.Index)
}
}
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)
}
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()
/*
// 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.
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
},
}
*/
return func(ctx context.Context) {
// // 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)
// init the request.
b.BeginRequest(ctx)
methodFunc := c.Method(methodFuncIndex).Interface().(func())
// execute the controls by order, including the method control.
for _, control := range t.controls {
if ctx.IsStopped() {
break
}
control.Handle(ctx, c, methodFunc)
}
// finally, execute the controller, don't check for IsStopped.
b.EndRequest(ctx)
}
}
// RegisterFunc used by the caller to register the result routes.
type RegisterFunc func(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) {
// 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 t.Methods {
registerFunc(m.HTTPMethod, buildMethodHandler(t, m.Index))
}
}
// 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,
registerFunc RegisterFunc) error {
t, err := ActivateController(controller, bindValues, controls)
if err != nil {
return err
}
RegisterMethodHandlers(t, registerFunc)
return nil
}

118
mvc/activator/binder.go Normal file
View File

@@ -0,0 +1,118 @@
package activator
import (
"reflect"
)
type binder struct {
values []interface{}
fields []field
}
// 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
}
b := &binder{values: values}
b.fields = b.lookup(elemType)
// if nothing valid found return nil, so the caller
// can omit the binder.
if len(b.fields) == 0 {
return nil
}
return b
}
func (b *binder) lookup(elem reflect.Type) (fields []field) {
for _, v := range b.values {
value := reflect.ValueOf(v)
for i, n := 0, elem.NumField(); i < n; i++ {
elemField := elem.Field(i)
if elemField.Type == value.Type() {
// we area inside the correct type
// println("[0] prepare bind filed for " + elemField.Name)
fields = append(fields, field{
Index: i,
Name: elemField.Name,
Type: elemField.Type,
Value: value,
})
continue
}
f := lookupStruct(elemField.Type, value)
if f != nil {
fields = append(fields, field{
Index: i,
Name: elemField.Name,
Type: elemField.Type,
embedded: f,
})
}
}
}
return
}
func lookupStruct(elem reflect.Type, value reflect.Value) *field {
// ignore if that field is not a struct
if elem.Kind() != reflect.Struct {
// and it's not a controller because we don't want to accidentally
// set fields to other user fields. Or no?
// ||
// (elem.Name() != "" && !strings.HasSuffix(elem.Name(), "Controller")) {
return nil
}
// search by fields.
for i, n := 0, elem.NumField(); i < n; i++ {
elemField := elem.Field(i)
if elemField.Type == value.Type() {
// println("Types are equal of: " + elemField.Type.Name() + " " + elemField.Name + " and " + value.Type().Name())
// we area inside the correct type.
return &field{
Index: i,
Name: elemField.Name,
Type: elemField.Type,
Value: value,
}
}
// 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 && value.Type().Kind() == reflect.Struct {
elemFieldEmb := elem.Field(i)
f := lookupStruct(elemFieldEmb.Type, value)
if f != nil {
fp := &field{
Index: i,
Name: elemFieldEmb.Name,
Type: elemFieldEmb.Type,
embedded: f,
}
return fp
}
}
}
return nil
}
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)
}
}

View File

@@ -0,0 +1,53 @@
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

@@ -0,0 +1,81 @@
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,54 @@
package activator
import (
"reflect"
"github.com/kataras/iris/context"
)
type modelControl struct {
fields []field
}
func (mc *modelControl) Load(t *TController) error {
fields := lookupFields(t, func(f reflect.StructField) bool {
if tag, ok := f.Tag.Lookup("iris"); ok {
if tag == "model" {
return true
}
}
return false
})
if len(fields) == 0 {
// first is the `Controller` so we need to
// check the second and after that.
return ErrControlSkip
}
mc.fields = fields
return nil
}
func (mc *modelControl) Handle(ctx context.Context, c reflect.Value, methodFunc func()) {
elem := c.Elem() // controller should always be a pointer at this state
for _, f := range mc.fields {
elemField := elem.Field(f.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() != f.Type {
continue
}
fieldValue := elemField.Interface()
// fmt.Printf("setting %s to %#v", f.Name, fieldValue)
ctx.ViewData(f.Name, fieldValue)
}
}
// 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,103 @@
package activator
import (
"reflect"
"github.com/kataras/iris/context"
)
type field struct {
Name string // by-defaultis the field's name but if `name: "other"` then it's overridden.
Index int
Type reflect.Type
Value reflect.Value
embedded *field
}
func (ff field) sendTo(elem reflect.Value) {
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)
}
func lookupFields(t *TController, validator func(reflect.StructField) bool) (fields []field) {
elem := t.Type.Elem()
for i, n := 0, elem.NumField(); i < n; i++ {
elemField := elem.Field(i)
valF := t.Value.Field(i)
// catch persistence data by tags, i.e:
// MyData string `iris:"persistence"`
if validator(elemField) {
name := elemField.Name
if nameTag, ok := elemField.Tag.Lookup("name"); ok {
name = nameTag
}
f := field{
Name: name,
Index: i,
Type: elemField.Type,
}
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 = append(fields, f)
}
}
return
}
type persistenceDataControl struct {
fields []field
}
func (d *persistenceDataControl) Load(t *TController) error {
fields := lookupFields(t, func(f reflect.StructField) bool {
if tag, ok := f.Tag.Lookup("iris"); ok {
if tag == "persistence" {
return true
}
}
return false
})
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{}
}