mirror of
https://github.com/kataras/iris.git
synced 2025-12-17 18:07:01 +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:
312
mvc/activator/activator.go
Normal file
312
mvc/activator/activator.go
Normal 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
118
mvc/activator/binder.go
Normal 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)
|
||||
}
|
||||
}
|
||||
53
mvc/activator/callable_control.go
Normal file
53
mvc/activator/callable_control.go
Normal 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}
|
||||
}
|
||||
81
mvc/activator/method_control.go
Normal file
81
mvc/activator/method_control.go
Normal 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{}
|
||||
}
|
||||
54
mvc/activator/model_control.go
Normal file
54
mvc/activator/model_control.go
Normal 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{}
|
||||
}
|
||||
103
mvc/activator/persistence_data_control.go
Normal file
103
mvc/activator/persistence_data_control.go
Normal 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{}
|
||||
}
|
||||
129
mvc/controller.go
Normal file
129
mvc/controller.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package mvc
|
||||
|
||||
import (
|
||||
"github.com/kataras/iris/context"
|
||||
"github.com/kataras/iris/core/memstore"
|
||||
"github.com/kataras/iris/mvc/activator"
|
||||
)
|
||||
|
||||
// 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())
|
||||
//
|
||||
// Look `core/router/APIBuilder#Controller` method too.
|
||||
type Controller struct {
|
||||
// path and path params.
|
||||
Path string
|
||||
Params *context.RequestParams
|
||||
|
||||
// some info read and write,
|
||||
// can be already set-ed by previous handlers as well.
|
||||
Status int
|
||||
Values *memstore.Store
|
||||
|
||||
// view read and write,
|
||||
// can be already set-ed by previous handlers as well.
|
||||
Layout string
|
||||
Tmpl string
|
||||
Data map[string]interface{}
|
||||
|
||||
// give access to the request context itself.
|
||||
Ctx context.Context
|
||||
}
|
||||
|
||||
// BeginRequest starts the main controller
|
||||
// it initialize the Ctx and other fields.
|
||||
//
|
||||
// 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
|
||||
c.Data = make(map[string]interface{}, 0)
|
||||
// context itself
|
||||
c.Ctx = ctx
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// End-Developer can ovveride it but still should be called at the end.
|
||||
func (c *Controller) EndRequest(ctx context.Context) {
|
||||
if path := c.Path; path != "" && path != ctx.Path() {
|
||||
// then redirect
|
||||
ctx.Redirect(path)
|
||||
return
|
||||
}
|
||||
|
||||
if status := c.Status; status > 0 && status != ctx.GetStatusCode() {
|
||||
ctx.StatusCode(status)
|
||||
}
|
||||
|
||||
if view := c.Tmpl; view != "" {
|
||||
if layout := c.Layout; layout != "" {
|
||||
ctx.ViewLayout(layout)
|
||||
}
|
||||
if data := c.Data; data != nil {
|
||||
for k, v := range data {
|
||||
ctx.ViewData(k, v)
|
||||
}
|
||||
}
|
||||
ctx.View(view)
|
||||
}
|
||||
}
|
||||
|
||||
var _ activator.BaseController = &Controller{}
|
||||
284
mvc/controller_test.go
Normal file
284
mvc/controller_test.go
Normal file
@@ -0,0 +1,284 @@
|
||||
// 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/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(httptest.StatusOK).
|
||||
Body().Equal(method)
|
||||
|
||||
e.Request(method, "/all").Expect().Status(httptest.StatusOK).
|
||||
Body().Equal(method)
|
||||
|
||||
e.Request(method, "/any").Expect().Status(httptest.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(httptest.StatusOK).
|
||||
Body().Equal(method)
|
||||
|
||||
e.Request(method, "/path1").Expect().Status(httptest.StatusOK).
|
||||
Body().Equal(method)
|
||||
|
||||
e.Request(method, "/path2").Expect().Status(httptest.StatusOK).
|
||||
Body().Equal(method)
|
||||
}
|
||||
}
|
||||
|
||||
type testControllerPersistence struct {
|
||||
mvc.Controller
|
||||
Data string `iris:"persistence"`
|
||||
}
|
||||
|
||||
func (t *testControllerPersistence) Get() {
|
||||
t.Ctx.WriteString(t.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(httptest.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 (t *testControllerBeginAndEndRequestFunc) BeginRequest(ctx context.Context) {
|
||||
t.Controller.BeginRequest(ctx)
|
||||
t.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 (t *testControllerBeginAndEndRequestFunc) EndRequest(ctx context.Context) {
|
||||
ctx.Writef("done") // append "done" to the response
|
||||
t.Controller.EndRequest(ctx)
|
||||
}
|
||||
|
||||
func (t *testControllerBeginAndEndRequestFunc) Get() {
|
||||
t.Ctx.Writef(t.Username)
|
||||
}
|
||||
|
||||
func (t *testControllerBeginAndEndRequestFunc) Post() {
|
||||
t.Ctx.Writef(t.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(httptest.StatusOK).
|
||||
Body().Equal(username + doneResponse)
|
||||
e.POST("/profile/" + username).Expect().Status(httptest.StatusOK).
|
||||
Body().Equal(username + doneResponse)
|
||||
}
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
Username string
|
||||
}
|
||||
|
||||
type testControllerModel struct {
|
||||
mvc.Controller
|
||||
|
||||
TestModel Model `iris:"model" name:"myModel"`
|
||||
TestModel2 Model `iris:"model"`
|
||||
}
|
||||
|
||||
func (t *testControllerModel) Get() {
|
||||
username := t.Ctx.Params().Get("username")
|
||||
t.TestModel = Model{Username: username}
|
||||
t.TestModel2 = Model{Username: username + "2"}
|
||||
}
|
||||
|
||||
func (t *testControllerModel) EndRequest(ctx context.Context) {
|
||||
// t.Ctx == ctx
|
||||
|
||||
m, ok := t.Ctx.GetViewData()["myModel"]
|
||||
if !ok {
|
||||
t.Ctx.Writef("fail TestModel load and set")
|
||||
return
|
||||
}
|
||||
|
||||
model, ok := m.(Model)
|
||||
|
||||
if !ok {
|
||||
t.Ctx.Writef("fail to override the TestModel name by the tag")
|
||||
return
|
||||
}
|
||||
|
||||
// test without custom name tag, should have the field's nae.
|
||||
m, ok = t.Ctx.GetViewData()["TestModel2"]
|
||||
if !ok {
|
||||
t.Ctx.Writef("fail TestModel2 load and set")
|
||||
return
|
||||
}
|
||||
|
||||
model2, ok := m.(Model)
|
||||
|
||||
if !ok {
|
||||
t.Ctx.Writef("fail to override the TestModel2 name by the tag")
|
||||
return
|
||||
}
|
||||
|
||||
// models are being rendered via the View at ViewData but
|
||||
// we just test it here, so print it back.
|
||||
t.Ctx.Writef(model.Username + model2.Username)
|
||||
|
||||
t.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(httptest.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()
|
||||
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(httptest.StatusOK).
|
||||
Body().Equal(expected)
|
||||
e.GET("/deep").Expect().Status(httptest.StatusOK).
|
||||
Body().Equal(expected)
|
||||
}
|
||||
35
mvc/session_controller.go
Normal file
35
mvc/session_controller.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package mvc
|
||||
|
||||
import (
|
||||
"github.com/kataras/iris/context"
|
||||
"github.com/kataras/iris/sessions"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
Controller
|
||||
|
||||
Manager *sessions.Sessions
|
||||
Session *sessions.Session
|
||||
}
|
||||
|
||||
var managerMissing = "MVC SessionController: session manager field is nil, you have to bind it to a *sessions.Sessions"
|
||||
|
||||
// BeginRequest calls the Controller's BeginRequest
|
||||
// and tries to initialize the current user's Session.
|
||||
func (s *SessionController) BeginRequest(ctx context.Context) {
|
||||
s.Controller.BeginRequest(ctx)
|
||||
if s.Manager == nil {
|
||||
ctx.Application().Logger().Errorf(managerMissing)
|
||||
return
|
||||
}
|
||||
|
||||
s.Session = s.Manager.Start(ctx)
|
||||
}
|
||||
|
||||
/* TODO:
|
||||
Maybe add struct tags on `binder` for required binded values
|
||||
in order to log error if some of the bindings are missing or leave that to the end-developers?
|
||||
*/
|
||||
Reference in New Issue
Block a user