mirror of
https://github.com/kataras/iris.git
synced 2025-02-02 15:30:36 +01:00
cm
Former-commit-id: 8f99121b81dc76c04d5910117885d9286873f26c
This commit is contained in:
parent
7043f352d9
commit
a7b2a90e3b
4
mvc2/binder/binder/input.go
Normal file
4
mvc2/binder/binder/input.go
Normal file
|
@ -0,0 +1,4 @@
|
|||
package binder
|
||||
|
||||
type Input interface {
|
||||
}
|
19
mvc2/binder/binding.go
Normal file
19
mvc2/binder/binding.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package binder
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type Binding interface {
|
||||
AddSource(v reflect.Value, source ...reflect.Value)
|
||||
}
|
||||
|
||||
type StructValue struct {
|
||||
Type reflect.Type
|
||||
Value reflect.Value
|
||||
}
|
||||
|
||||
type FuncResultValue struct {
|
||||
Type reflect.Type
|
||||
ReturnValue func(ctx []reflect.Value) reflect.Value
|
||||
}
|
1
mvc2/binder/func_input.go
Normal file
1
mvc2/binder/func_input.go
Normal file
|
@ -0,0 +1 @@
|
|||
package binder
|
53
mvc2/binder/func_result.go
Normal file
53
mvc2/binder/func_result.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package binder
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
var (
|
||||
errBad = errors.New("bad")
|
||||
)
|
||||
|
||||
func makeReturnValue(fn reflect.Value) (func([]reflect.Value) reflect.Value, reflect.Type, error) {
|
||||
typ := indirectTyp(fn.Type())
|
||||
|
||||
// invalid if not a func.
|
||||
if typ.Kind() != reflect.Func {
|
||||
return nil, typ, errBad
|
||||
}
|
||||
|
||||
// invalid if not returns one single value.
|
||||
if typ.NumOut() != 1 {
|
||||
return nil, typ, errBad
|
||||
}
|
||||
|
||||
// invalid if input args length is not one.
|
||||
if typ.NumIn() != 1 {
|
||||
return nil, typ, errBad
|
||||
}
|
||||
|
||||
// invalid if that single input arg is not a typeof context.Context.
|
||||
if !isContext(typ.In(0)) {
|
||||
return nil, typ, errBad
|
||||
}
|
||||
|
||||
outTyp := typ.Out(0)
|
||||
zeroOutVal := reflect.New(outTyp).Elem()
|
||||
|
||||
bf := func(ctxValue []reflect.Value) reflect.Value {
|
||||
// []reflect.Value{reflect.ValueOf(ctx)}
|
||||
results := fn.Call(ctxValue) // ctxValue is like that because of; read makeHandler.
|
||||
if len(results) == 0 {
|
||||
return zeroOutVal
|
||||
}
|
||||
|
||||
v := results[0]
|
||||
if !v.IsValid() {
|
||||
return zeroOutVal
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
return bf, outTyp, nil
|
||||
}
|
107
mvc2/binder/reflect.go
Normal file
107
mvc2/binder/reflect.go
Normal file
|
@ -0,0 +1,107 @@
|
|||
package binder
|
||||
|
||||
import "reflect"
|
||||
|
||||
func isContext(inTyp reflect.Type) bool {
|
||||
return inTyp.String() == "context.Context" // I couldn't find another way; context/context.go is not exported.
|
||||
}
|
||||
|
||||
func indirectVal(v reflect.Value) reflect.Value {
|
||||
return reflect.Indirect(v)
|
||||
}
|
||||
|
||||
func indirectTyp(typ reflect.Type) reflect.Type {
|
||||
switch typ.Kind() {
|
||||
case reflect.Ptr, reflect.Array, reflect.Chan, reflect.Map, reflect.Slice:
|
||||
return typ.Elem()
|
||||
}
|
||||
return typ
|
||||
}
|
||||
|
||||
func goodVal(v reflect.Value) bool {
|
||||
switch v.Kind() {
|
||||
case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Interface, reflect.Slice:
|
||||
if v.IsNil() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return v.IsValid()
|
||||
}
|
||||
|
||||
func isFunc(typ reflect.Type) bool {
|
||||
return typ.Kind() == reflect.Func
|
||||
}
|
||||
|
||||
/*
|
||||
// no f. this, it's too complicated and it will be harder to maintain later on:
|
||||
func isSliceAndExpectedItem(got reflect.Type, in []reflect.Type, currentBindersIdx int) bool {
|
||||
kind := got.Kind()
|
||||
// if got result is slice or array.
|
||||
return (kind == reflect.Slice || kind == reflect.Array) &&
|
||||
// if has expected next input.
|
||||
len(in)-1 > currentBindersIdx &&
|
||||
// if the current input's type is not the same as got (if it's not a slice of that types or anything else).
|
||||
equalTypes(got, in[currentBindersIdx])
|
||||
}
|
||||
*/
|
||||
|
||||
func equalTypes(got reflect.Type, expected reflect.Type) bool {
|
||||
if got == expected {
|
||||
return true
|
||||
}
|
||||
// if accepts an interface, check if the given "got" type does
|
||||
// implement this "expected" user handler's input argument.
|
||||
if expected.Kind() == reflect.Interface {
|
||||
// fmt.Printf("expected interface = %s and got to set on the arg is: %s\n", expected.String(), got.String())
|
||||
return got.Implements(expected)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// for controller only.
|
||||
|
||||
func structFieldIgnored(f reflect.StructField) bool {
|
||||
if !f.Anonymous {
|
||||
return true // if not anonymous(embedded), ignore it.
|
||||
}
|
||||
|
||||
s := f.Tag.Get("ignore")
|
||||
return s == "true" // if has an ignore tag then ignore it.
|
||||
}
|
||||
|
||||
type field struct {
|
||||
Type reflect.Type
|
||||
Index []int // the index of the field, slice if it's part of a embedded struct
|
||||
Name string // the actual name
|
||||
|
||||
// this could be empty, but in our cases it's not,
|
||||
// it's filled with the service and it's filled from the lookupFields' caller.
|
||||
AnyValue reflect.Value
|
||||
}
|
||||
|
||||
func lookupFields(typ reflect.Type, parentIndex int) (fields []field) {
|
||||
for i, n := 0, typ.NumField(); i < n; i++ {
|
||||
f := typ.Field(i)
|
||||
|
||||
if f.Type.Kind() == reflect.Struct && !structFieldIgnored(f) {
|
||||
fields = append(fields, lookupFields(f.Type, i)...)
|
||||
continue
|
||||
}
|
||||
|
||||
index := []int{i}
|
||||
if parentIndex >= 0 {
|
||||
index = append([]int{parentIndex}, index...)
|
||||
}
|
||||
|
||||
field := field{
|
||||
Type: f.Type,
|
||||
Name: f.Name,
|
||||
Index: index,
|
||||
}
|
||||
|
||||
fields = append(fields, field)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
50
mvc2/binder/to_struct.go
Normal file
50
mvc2/binder/to_struct.go
Normal file
|
@ -0,0 +1,50 @@
|
|||
package binder
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type StructBinding struct {
|
||||
Field StructValue
|
||||
Func FuncResultValue
|
||||
}
|
||||
|
||||
func (b *StructBinding) AddSource(dest reflect.Value, source ...reflect.Value) {
|
||||
typ := indirectTyp(dest.Type()) //indirectTyp(reflect.TypeOf(dest))
|
||||
if typ.Kind() != reflect.Struct {
|
||||
return
|
||||
}
|
||||
|
||||
fields := lookupFields(typ, -1)
|
||||
for _, f := range fields {
|
||||
for _, s := range source {
|
||||
if s.Type().Kind() == reflect.Func {
|
||||
returnValue, outType, err := makeReturnValue(s)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
gotTyp = outType
|
||||
service.ReturnValue = returnValue
|
||||
}
|
||||
|
||||
gotTyp := s.Type()
|
||||
|
||||
v := StructValue{
|
||||
Type: gotTyp,
|
||||
Value: s,
|
||||
FieldIndex: f.Index,
|
||||
}
|
||||
|
||||
if equalTypes(gotTyp, f.Type) {
|
||||
service.Type = gotTyp
|
||||
_serv = append(_serv, &service)
|
||||
fmt.Printf("[2] Bind In=%s->%s for struct field[%d]\n", f.Type, gotTyp.String(), f.Index)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Printf("[2] Bind %d for %s\n", len(_serv), typ.String())
|
||||
*serv = _serv
|
||||
|
||||
return
|
||||
}
|
173
mvc2/binder_in.go
Normal file
173
mvc2/binder_in.go
Normal file
|
@ -0,0 +1,173 @@
|
|||
package mvc2
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// InputBinder is the result of `MakeBinder`.
|
||||
// It contains the binder wrapped information, like the
|
||||
// type that is responsible to bind
|
||||
// and a function which will accept a context and returns a value of something.
|
||||
type InputBinder struct {
|
||||
BinderType binderType
|
||||
BindType reflect.Type
|
||||
BindFunc func(ctx []reflect.Value) reflect.Value
|
||||
}
|
||||
|
||||
// key = the func input argument index, value is the responsible input binder.
|
||||
type bindersMap map[int]*InputBinder
|
||||
|
||||
// joinBindersMap joins the "m2" to m1 and returns the result, it's the same "m1" map.
|
||||
// if "m2" is not nil and "m2" is not nil then it loops the "m2"'s keys and sets the values
|
||||
// to the "m1", if "m2" is not and not empty nil but m1 is nil then "m1" = "m2".
|
||||
// The result may be nil if the "m1" and "m2" are nil or "m2" is empty and "m1" is nil.
|
||||
func joinBindersMap(m1, m2 bindersMap) bindersMap {
|
||||
if m2 != nil && len(m2) > 0 {
|
||||
if m1 == nil {
|
||||
m1 = m2
|
||||
} else {
|
||||
for k, v := range m2 {
|
||||
m1[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
return m1
|
||||
}
|
||||
|
||||
// getBindersForInput returns a map of the responsible binders for the "expected" types,
|
||||
// which are the expected input parameters' types,
|
||||
// based on the available "binders" collection.
|
||||
//
|
||||
// It returns a map which its key is the index of the "expected" which
|
||||
// a valid binder for that in's type found,
|
||||
// the value is the pointer of the responsible `InputBinder`.
|
||||
//
|
||||
// Check of "a nothing responsible for those expected types"
|
||||
// should be done using the `len(m) == 0`.
|
||||
func getBindersForInput(binders []*InputBinder, expected ...reflect.Type) (m bindersMap) {
|
||||
for idx, in := range expected {
|
||||
if idx == 0 && isContext(in) {
|
||||
// if the first is context then set it directly here.
|
||||
m = make(bindersMap)
|
||||
m[0] = &InputBinder{
|
||||
BindType: contextTyp,
|
||||
BindFunc: func(ctxValues []reflect.Value) reflect.Value {
|
||||
return ctxValues[0]
|
||||
},
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
for _, b := range binders {
|
||||
if equalTypes(b.BindType, in) {
|
||||
if m == nil {
|
||||
m = make(bindersMap)
|
||||
}
|
||||
// fmt.Printf("set index: %d to type: %s where input type is: %s\n", idx, b.BindType.String(), in.String())
|
||||
m[idx] = b
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// MustMakeFuncInputBinder calls the `MakeFuncInputBinder` and returns its first result, see its docs.
|
||||
// It panics on error.
|
||||
func MustMakeFuncInputBinder(binder interface{}) *InputBinder {
|
||||
b, err := MakeFuncInputBinder(binder)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
type binderType uint32
|
||||
|
||||
const (
|
||||
functionType binderType = iota
|
||||
serviceType
|
||||
invalidType
|
||||
)
|
||||
|
||||
func resolveBinderType(binder interface{}) binderType {
|
||||
if binder == nil {
|
||||
return invalidType
|
||||
}
|
||||
|
||||
return resolveBinderTypeFromKind(reflect.TypeOf(binder).Kind())
|
||||
}
|
||||
|
||||
func resolveBinderTypeFromKind(k reflect.Kind) binderType {
|
||||
switch k {
|
||||
case reflect.Func:
|
||||
return functionType
|
||||
case reflect.Struct, reflect.Interface, reflect.Ptr, reflect.Slice, reflect.Array:
|
||||
return serviceType
|
||||
}
|
||||
|
||||
return invalidType
|
||||
}
|
||||
|
||||
// MakeFuncInputBinder takes a binder function or a struct which contains a "Bind"
|
||||
// function and returns an `InputBinder`, which Iris uses to
|
||||
// resolve and set the input parameters when a handler is executed.
|
||||
//
|
||||
// The "binder" can have the following form:
|
||||
// `func(iris.Context) UserViewModel`.
|
||||
//
|
||||
// The return type of the "binder" should be a value instance, not a pointer, for your own protection.
|
||||
// The binder function should return only one value and
|
||||
// it can accept only one input argument, the Iris' Context (`context.Context` or `iris.Context`).
|
||||
func MakeFuncInputBinder(binder interface{}) (*InputBinder, error) {
|
||||
v := reflect.ValueOf(binder)
|
||||
return makeFuncInputBinder(v)
|
||||
}
|
||||
|
||||
func makeFuncInputBinder(fn reflect.Value) (*InputBinder, error) {
|
||||
typ := indirectTyp(fn.Type())
|
||||
|
||||
// invalid if not a func.
|
||||
if typ.Kind() != reflect.Func {
|
||||
return nil, errBad
|
||||
}
|
||||
|
||||
// invalid if not returns one single value.
|
||||
if typ.NumOut() != 1 {
|
||||
return nil, errBad
|
||||
}
|
||||
|
||||
// invalid if input args length is not one.
|
||||
if typ.NumIn() != 1 {
|
||||
return nil, errBad
|
||||
}
|
||||
|
||||
// invalid if that single input arg is not a typeof context.Context.
|
||||
if !isContext(typ.In(0)) {
|
||||
return nil, errBad
|
||||
}
|
||||
|
||||
outTyp := typ.Out(0)
|
||||
zeroOutVal := reflect.New(outTyp).Elem()
|
||||
|
||||
bf := func(ctxValue []reflect.Value) reflect.Value {
|
||||
// []reflect.Value{reflect.ValueOf(ctx)}
|
||||
results := fn.Call(ctxValue) // ctxValue is like that because of; read makeHandler.
|
||||
if len(results) == 0 {
|
||||
return zeroOutVal
|
||||
}
|
||||
|
||||
v := results[0]
|
||||
if !v.IsValid() {
|
||||
return zeroOutVal
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
return &InputBinder{
|
||||
BinderType: functionType,
|
||||
BindType: outTyp,
|
||||
BindFunc: bf,
|
||||
}, nil
|
||||
}
|
135
mvc2/binder_in_path_param.go
Normal file
135
mvc2/binder_in_path_param.go
Normal file
|
@ -0,0 +1,135 @@
|
|||
package mvc2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/kataras/iris/context"
|
||||
"github.com/kataras/iris/core/memstore"
|
||||
"github.com/kataras/iris/core/router/macro"
|
||||
"github.com/kataras/iris/core/router/macro/interpreter/ast"
|
||||
)
|
||||
|
||||
func getInputArgsFromFunc(funcTyp reflect.Type) []reflect.Type {
|
||||
n := funcTyp.NumIn()
|
||||
funcIn := make([]reflect.Type, n, n)
|
||||
for i := 0; i < n; i++ {
|
||||
funcIn[i] = funcTyp.In(i)
|
||||
}
|
||||
return funcIn
|
||||
}
|
||||
|
||||
func getPathParamsForInput(params []macro.TemplateParam, funcIn ...reflect.Type) (values []reflect.Value) {
|
||||
if len(funcIn) == 0 || len(params) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
funcInIdx := 0
|
||||
// it's a valid param type.
|
||||
for _, p := range params {
|
||||
in := funcIn[funcInIdx]
|
||||
paramType := p.Type
|
||||
paramName := p.Name
|
||||
|
||||
// fmt.Printf("%s input arg type vs %s param type\n", in.Kind().String(), p.Type.Kind().String())
|
||||
if p.Type.Assignable(in.Kind()) {
|
||||
|
||||
// b = append(b, &InputBinder{
|
||||
// BindType: in, // or p.Type.Kind, should be the same.
|
||||
// BindFunc: func(ctx []reflect.Value) reflect.Value {
|
||||
// // I don't like this ctx[0].Interface(0)
|
||||
// // it will be slow, and silly because we have ctx already
|
||||
// // before the bindings at serve-time, so we will create
|
||||
// // a func for each one of the param types, they are just 4 so
|
||||
// // it worths some dublications.
|
||||
// return getParamValueFromType(ctx[0].Interface(), paramType, paramName)
|
||||
// },
|
||||
// })
|
||||
|
||||
var fn interface{}
|
||||
|
||||
if paramType == ast.ParamTypeInt {
|
||||
fn = func(ctx context.Context) int {
|
||||
v, _ := ctx.Params().GetInt(paramName)
|
||||
return v
|
||||
}
|
||||
} else if paramType == ast.ParamTypeLong {
|
||||
fn = func(ctx context.Context) int64 {
|
||||
v, _ := ctx.Params().GetInt64(paramName)
|
||||
return v
|
||||
}
|
||||
|
||||
} else if paramType == ast.ParamTypeBoolean {
|
||||
fn = func(ctx context.Context) bool {
|
||||
v, _ := ctx.Params().GetBool(paramName)
|
||||
return v
|
||||
}
|
||||
|
||||
} else {
|
||||
// string, path...
|
||||
fn = func(ctx context.Context) string {
|
||||
return ctx.Params().Get(paramName)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("binder_in_path_param.go: bind path param func for paramName = '%s' and paramType = '%s'\n", paramName, paramType.String())
|
||||
values = append(values, reflect.ValueOf(fn))
|
||||
|
||||
// inputBinder, err := MakeFuncInputBinder(fn)
|
||||
// if err != nil {
|
||||
// fmt.Printf("err on make func binder: %v\n", err.Error())
|
||||
// continue
|
||||
// }
|
||||
|
||||
// if m == nil {
|
||||
// m = make(bindersMap, 0)
|
||||
// }
|
||||
|
||||
// // fmt.Printf("set param input binder for func arg index: %d\n", funcInIdx)
|
||||
// m[funcInIdx] = inputBinder
|
||||
}
|
||||
|
||||
funcInIdx++
|
||||
}
|
||||
|
||||
return
|
||||
// return m
|
||||
}
|
||||
|
||||
// PathParams is the context's named path parameters, see `PathParamsBinder` too.
|
||||
type PathParams = context.RequestParams
|
||||
|
||||
// PathParamsBinder is the binder which will bind the `PathParams` type value to the specific
|
||||
// handler's input argument, see `PathParams` as well.
|
||||
func PathParamsBinder(ctx context.Context) PathParams {
|
||||
return *ctx.Params()
|
||||
}
|
||||
|
||||
// PathParam describes a named path parameter, it's the result of the PathParamBinder and the expected
|
||||
// handler func's input argument's type, see `PathParamBinder` too.
|
||||
type PathParam struct {
|
||||
memstore.Entry
|
||||
Empty bool
|
||||
}
|
||||
|
||||
// PathParamBinder is the binder which binds a handler func's input argument to a named path parameter
|
||||
// based on its name, see `PathParam` as well.
|
||||
func PathParamBinder(name string) func(ctx context.Context) PathParam {
|
||||
return func(ctx context.Context) PathParam {
|
||||
e, found := ctx.Params().GetEntry(name)
|
||||
if !found {
|
||||
|
||||
// useless check here but it doesn't hurt,
|
||||
// useful only when white-box tests run.
|
||||
if ctx.Application() != nil {
|
||||
ctx.Application().Logger().Warnf(ctx.HandlerName()+": expected parameter name '%s' to be described in the route's path in order to be received by the `ParamBinder`, please fix it.\n The main handler will not be executed for your own protection.", name)
|
||||
}
|
||||
|
||||
ctx.StopExecution()
|
||||
return PathParam{
|
||||
Empty: true,
|
||||
}
|
||||
}
|
||||
return PathParam{e, false}
|
||||
}
|
||||
}
|
64
mvc2/binder_in_path_param_test.go
Normal file
64
mvc2/binder_in_path_param_test.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
package mvc2
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/kataras/iris/context"
|
||||
)
|
||||
|
||||
func TestPathParamsBinder(t *testing.T) {
|
||||
m := New().Bind(PathParamsBinder)
|
||||
|
||||
got := ""
|
||||
|
||||
h := m.Handler(func(params PathParams) {
|
||||
got = params.Get("firstname") + params.Get("lastname")
|
||||
})
|
||||
|
||||
ctx := context.NewContext(nil)
|
||||
ctx.Params().Set("firstname", "Gerasimos")
|
||||
ctx.Params().Set("lastname", "Maropoulos")
|
||||
h(ctx)
|
||||
expected := "GerasimosMaropoulos"
|
||||
if got != expected {
|
||||
t.Fatalf("expected the params 'firstname' + 'lastname' to be '%s' but got '%s'", expected, got)
|
||||
}
|
||||
}
|
||||
func TestPathParamBinder(t *testing.T) {
|
||||
m := New().Bind(PathParamBinder("username"))
|
||||
|
||||
got := ""
|
||||
executed := false
|
||||
h := m.Handler(func(username PathParam) {
|
||||
// this should not be fired at all if "username" param wasn't found at all.
|
||||
// although router is responsible for that but the `ParamBinder` makes that check as well because
|
||||
// the end-developer may put a param as input argument on her/his function but
|
||||
// on its route's path didn't describe the path parameter,
|
||||
// the handler fires a warning and stops the execution for the invalid handler to protect the user.
|
||||
executed = true
|
||||
got = username.String()
|
||||
})
|
||||
|
||||
expectedUsername := "kataras"
|
||||
ctx := context.NewContext(nil)
|
||||
ctx.Params().Set("username", expectedUsername)
|
||||
h(ctx)
|
||||
|
||||
if got != expectedUsername {
|
||||
t.Fatalf("expected the param 'username' to be '%s' but got '%s'", expectedUsername, got)
|
||||
}
|
||||
|
||||
// test the non executed if param not found.
|
||||
executed = false
|
||||
got = ""
|
||||
|
||||
ctx2 := context.NewContext(nil)
|
||||
h(ctx2)
|
||||
|
||||
if got != "" {
|
||||
t.Fatalf("expected the param 'username' to be entirely empty but got '%s'", got)
|
||||
}
|
||||
if executed {
|
||||
t.Fatalf("expected the handler to not be executed")
|
||||
}
|
||||
}
|
81
mvc2/binder_in_service.go
Normal file
81
mvc2/binder_in_service.go
Normal file
|
@ -0,0 +1,81 @@
|
|||
package mvc2
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type serviceFieldBinder struct {
|
||||
Index []int
|
||||
Binder *InputBinder
|
||||
}
|
||||
|
||||
func getServicesBinderForStruct(binders []*InputBinder, typ reflect.Type) func(elem reflect.Value) {
|
||||
fields := lookupFields(typ, -1)
|
||||
var validBinders []*serviceFieldBinder
|
||||
|
||||
for _, b := range binders {
|
||||
for _, f := range fields {
|
||||
if b.BinderType != serviceType {
|
||||
continue
|
||||
}
|
||||
if equalTypes(b.BindType, f.Type) {
|
||||
validBinders = append(validBinders,
|
||||
&serviceFieldBinder{Index: f.Index, Binder: b})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if len(validBinders) == 0 {
|
||||
return func(_ reflect.Value) {}
|
||||
}
|
||||
|
||||
return func(elem reflect.Value) {
|
||||
for _, b := range validBinders {
|
||||
elem.FieldByIndex(b.Index).Set(b.Binder.BindFunc(nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MustMakeServiceInputBinder calls the `MakeServiceInputBinder` and returns its first result, see its docs.
|
||||
// It panics on error.
|
||||
func MustMakeServiceInputBinder(service interface{}) *InputBinder {
|
||||
s, err := MakeServiceInputBinder(service)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// MakeServiceInputBinder uses a difference/or strange approach,
|
||||
// we make the services as bind functions
|
||||
// in order to keep the rest of the code simpler, however we have
|
||||
// a performance penalty when calling the function instead
|
||||
// of just put the responsible service to the certain handler's input argument.
|
||||
func MakeServiceInputBinder(service interface{}) (*InputBinder, error) {
|
||||
if service == nil {
|
||||
return nil, errNil
|
||||
}
|
||||
|
||||
var (
|
||||
val = reflect.ValueOf(service)
|
||||
typ = val.Type()
|
||||
)
|
||||
|
||||
if !goodVal(val) {
|
||||
return nil, errBad
|
||||
}
|
||||
|
||||
if indirectTyp(typ).Kind() != reflect.Struct {
|
||||
// if the pointer's struct is not a struct then return err bad.
|
||||
return nil, errBad
|
||||
}
|
||||
|
||||
return &InputBinder{
|
||||
BinderType: serviceType,
|
||||
BindType: typ,
|
||||
BindFunc: func(_ []reflect.Value) reflect.Value {
|
||||
return val
|
||||
},
|
||||
}, nil
|
||||
}
|
46
mvc2/binder_in_service_test.go
Normal file
46
mvc2/binder_in_service_test.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
package mvc2
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type (
|
||||
testService interface {
|
||||
say(string)
|
||||
}
|
||||
testServiceImpl struct {
|
||||
prefix string
|
||||
}
|
||||
)
|
||||
|
||||
func (s *testServiceImpl) say(message string) string {
|
||||
return s.prefix + ": " + message
|
||||
}
|
||||
|
||||
func TestMakeServiceInputBinder(t *testing.T) {
|
||||
expectedService := &testServiceImpl{"say"}
|
||||
b := MustMakeServiceInputBinder(expectedService)
|
||||
// in
|
||||
var (
|
||||
intType = reflect.TypeOf(1)
|
||||
availableBinders = []*InputBinder{b}
|
||||
)
|
||||
|
||||
// 1
|
||||
testCheck(t, "test1", true, testGetBindersForInput(t, availableBinders,
|
||||
[]interface{}{expectedService}, reflect.TypeOf(expectedService)))
|
||||
// 2
|
||||
testCheck(t, "test2-fail", false, testGetBindersForInput(t, availableBinders,
|
||||
[]interface{}{42}))
|
||||
// 3
|
||||
testCheck(t, "test3-fail", false, testGetBindersForInput(t, availableBinders,
|
||||
[]interface{}{42}, intType))
|
||||
// 4
|
||||
testCheck(t, "test4-fail", false, testGetBindersForInput(t, availableBinders,
|
||||
[]interface{}{42}))
|
||||
// 5 - check if nothing passed, so no valid binders at all.
|
||||
testCheck(t, "test5", true, testGetBindersForInput(t, availableBinders,
|
||||
[]interface{}{}))
|
||||
|
||||
}
|
143
mvc2/binder_in_test.go
Normal file
143
mvc2/binder_in_test.go
Normal file
|
@ -0,0 +1,143 @@
|
|||
package mvc2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/kataras/iris/context"
|
||||
)
|
||||
|
||||
type testUserStruct struct {
|
||||
ID int64
|
||||
Username string
|
||||
}
|
||||
|
||||
func testBinderFunc(ctx context.Context) testUserStruct {
|
||||
id, _ := ctx.Params().GetInt64("id")
|
||||
username := ctx.Params().Get("username")
|
||||
return testUserStruct{
|
||||
ID: id,
|
||||
Username: username,
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeFuncInputBinder(t *testing.T) {
|
||||
testMakeFuncInputBinder(t, testBinderFunc)
|
||||
}
|
||||
|
||||
func testMakeFuncInputBinder(t *testing.T, binder interface{}) {
|
||||
b, err := MakeFuncInputBinder(binder)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to make binder: %v", err)
|
||||
}
|
||||
|
||||
if b == nil {
|
||||
t.Fatalf("excepted non-nil *InputBinder but got nil")
|
||||
}
|
||||
|
||||
if expected, got := reflect.TypeOf(testUserStruct{}), b.BindType; expected != got {
|
||||
t.Fatalf("expected type of the binder's return value to be: %T but got: %T", expected, got)
|
||||
}
|
||||
|
||||
expected := testUserStruct{
|
||||
ID: 42,
|
||||
Username: "kataras",
|
||||
}
|
||||
ctx := context.NewContext(nil)
|
||||
ctx.Params().Set("id", fmt.Sprintf("%v", expected.ID))
|
||||
ctx.Params().Set("username", expected.Username)
|
||||
ctxValue := []reflect.Value{reflect.ValueOf(ctx)}
|
||||
v := b.BindFunc(ctxValue)
|
||||
if !v.CanInterface() {
|
||||
t.Fatalf("result of binder func cannot be interfaced: %#+v", v)
|
||||
}
|
||||
|
||||
got, ok := v.Interface().(testUserStruct)
|
||||
if !ok {
|
||||
t.Fatalf("result of binder func should be a type of 'testUserStruct' but got: %#+v", v.Interface())
|
||||
}
|
||||
|
||||
if got != expected {
|
||||
t.Fatalf("invalid result of binder func, expected: %v but got: %v", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func testCheck(t *testing.T, testName string, shouldPass bool, errString string) {
|
||||
if shouldPass && errString != "" {
|
||||
t.Fatalf("[%s] %s", testName, errString)
|
||||
}
|
||||
if !shouldPass && errString == "" {
|
||||
t.Fatalf("[%s] expected not to pass", testName)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetBindersForInput will test two available binders, one for int
|
||||
// and other for a string,
|
||||
// the first input will contains both of them in the same order,
|
||||
// the second will contain both of them as well but with a different order,
|
||||
// the third will contain only the int input and should fail,
|
||||
// the forth one will contain only the string input and should fail,
|
||||
// the fifth one will contain two integers and should fail,
|
||||
// the last one will contain a struct and should fail,
|
||||
// that no of othe available binders will support it,
|
||||
// so no len of the result should be zero there.
|
||||
func TestGetBindersForInput(t *testing.T) {
|
||||
// binders
|
||||
var (
|
||||
stringBinder = MustMakeFuncInputBinder(func(ctx context.Context) string {
|
||||
return "a string"
|
||||
})
|
||||
intBinder = MustMakeFuncInputBinder(func(ctx context.Context) int {
|
||||
return 42
|
||||
})
|
||||
)
|
||||
// in
|
||||
var (
|
||||
stringType = reflect.TypeOf("string")
|
||||
intType = reflect.TypeOf(1)
|
||||
)
|
||||
|
||||
// 1
|
||||
testCheck(t, "test1", true, testGetBindersForInput(t, []*InputBinder{intBinder, stringBinder},
|
||||
[]interface{}{"a string", 42}, stringType, intType))
|
||||
availableBinders := []*InputBinder{stringBinder, intBinder} // different order than the fist test.
|
||||
// 2
|
||||
testCheck(t, "test2", true, testGetBindersForInput(t, availableBinders,
|
||||
[]interface{}{"a string", 42}, stringType, intType))
|
||||
// 3
|
||||
testCheck(t, "test-3-fail", false, testGetBindersForInput(t, availableBinders,
|
||||
[]interface{}{42}, stringType, intType))
|
||||
// 4
|
||||
testCheck(t, "test-4-fail", false, testGetBindersForInput(t, availableBinders,
|
||||
[]interface{}{"a string"}, stringType, intType))
|
||||
// 5
|
||||
testCheck(t, "test-5-fail", false, testGetBindersForInput(t, availableBinders,
|
||||
[]interface{}{42, 42}, stringType, intType))
|
||||
// 6
|
||||
testCheck(t, "test-6-fail", false, testGetBindersForInput(t, availableBinders,
|
||||
[]interface{}{testUserStruct{}}, stringType, intType))
|
||||
|
||||
}
|
||||
|
||||
func testGetBindersForInput(t *testing.T, binders []*InputBinder, expectingResults []interface{}, in ...reflect.Type) (errString string) {
|
||||
m := getBindersForInput(binders, in...)
|
||||
|
||||
if expected, got := len(expectingResults), len(m); expected != got {
|
||||
return fmt.Sprintf("expected results length(%d) and valid binders length(%d) to be equal, so each input has one binder", expected, got)
|
||||
}
|
||||
|
||||
ctxValue := []reflect.Value{reflect.ValueOf(context.NewContext(nil))}
|
||||
for idx, expected := range expectingResults {
|
||||
if m[idx] != nil {
|
||||
v := m[idx].BindFunc(ctxValue)
|
||||
if got := v.Interface(); got != expected {
|
||||
return fmt.Sprintf("expected result[%d] to be: %v but got: %v", idx, expected, got)
|
||||
}
|
||||
} else {
|
||||
t.Logf("m[%d] = nil on input = %v\n", idx, expected)
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
103
mvc2/engine.go
Normal file
103
mvc2/engine.go
Normal file
|
@ -0,0 +1,103 @@
|
|||
package mvc2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
|
||||
"github.com/kataras/iris/context"
|
||||
"github.com/kataras/iris/core/router"
|
||||
)
|
||||
|
||||
var (
|
||||
errNil = errors.New("nil")
|
||||
errBad = errors.New("bad")
|
||||
errAlreadyExists = errors.New("already exists")
|
||||
)
|
||||
|
||||
type Engine struct {
|
||||
binders []*InputBinder
|
||||
|
||||
Input []reflect.Value
|
||||
}
|
||||
|
||||
func New() *Engine {
|
||||
return new(Engine)
|
||||
}
|
||||
|
||||
func (e *Engine) Child() *Engine {
|
||||
child := New()
|
||||
|
||||
// copy the current parent's ctx func binders and services to this new child.
|
||||
// if l := len(e.binders); l > 0 {
|
||||
// binders := make([]*InputBinder, l, l)
|
||||
// copy(binders, e.binders)
|
||||
// child.binders = binders
|
||||
// }
|
||||
if l := len(e.Input); l > 0 {
|
||||
input := make([]reflect.Value, l, l)
|
||||
copy(input, e.Input)
|
||||
child.Input = input
|
||||
}
|
||||
return child
|
||||
}
|
||||
|
||||
func (e *Engine) Bind(binders ...interface{}) *Engine {
|
||||
for _, binder := range binders {
|
||||
// typ := resolveBinderType(binder)
|
||||
|
||||
// var (
|
||||
// b *InputBinder
|
||||
// err error
|
||||
// )
|
||||
|
||||
// if typ == functionType {
|
||||
// b, err = MakeFuncInputBinder(binder)
|
||||
// } else if typ == serviceType {
|
||||
// b, err = MakeServiceInputBinder(binder)
|
||||
// } else {
|
||||
// err = errBad
|
||||
// }
|
||||
|
||||
// if err != nil {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// e.binders = append(e.binders, b)
|
||||
|
||||
e.Input = append(e.Input, reflect.ValueOf(binder))
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// BindTypeExists returns true if a binder responsible to
|
||||
// bind and return a type of "typ" is already registered.
|
||||
func (e *Engine) BindTypeExists(typ reflect.Type) bool {
|
||||
// for _, b := range e.binders {
|
||||
// if equalTypes(b.BindType, typ) {
|
||||
// return true
|
||||
// }
|
||||
// }
|
||||
for _, in := range e.Input {
|
||||
if equalTypes(in.Type(), typ) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e *Engine) Handler(handler interface{}) context.Handler {
|
||||
h, _ := MakeHandler(handler, e.binders) // it logs errors already, so on any error the "h" will be nil.
|
||||
return h
|
||||
}
|
||||
|
||||
type ActivateListener interface {
|
||||
OnActivate(*ControllerActivator)
|
||||
}
|
||||
|
||||
func (e *Engine) Controller(router router.Party, controller BaseController) {
|
||||
ca := newControllerActivator(e, router, controller)
|
||||
if al, ok := controller.(ActivateListener); ok {
|
||||
al.OnActivate(ca)
|
||||
}
|
||||
}
|
422
mvc2/handler_out.go
Normal file
422
mvc2/handler_out.go
Normal file
|
@ -0,0 +1,422 @@
|
|||
package mvc2
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/structs"
|
||||
"github.com/kataras/iris/context"
|
||||
)
|
||||
|
||||
// Result is a response dispatcher.
|
||||
// All types that complete this interface
|
||||
// can be returned as values from the method functions.
|
||||
//
|
||||
// Example at: https://github.com/kataras/iris/tree/master/_examples/mvc/overview.
|
||||
type Result interface {
|
||||
// Dispatch should sends the response to the context's response writer.
|
||||
Dispatch(ctx context.Context)
|
||||
}
|
||||
|
||||
var defaultFailureResponse = Response{Code: DefaultErrStatusCode}
|
||||
|
||||
// Try will check if "fn" ran without any panics,
|
||||
// using recovery,
|
||||
// and return its result as the final response
|
||||
// otherwise it returns the "failure" response if any,
|
||||
// if not then a 400 bad request is being sent.
|
||||
//
|
||||
// Example usage at: https://github.com/kataras/iris/blob/master/mvc/method_result_test.go.
|
||||
func Try(fn func() Result, failure ...Result) Result {
|
||||
var failed bool
|
||||
var actionResponse Result
|
||||
|
||||
func() {
|
||||
defer func() {
|
||||
if rec := recover(); rec != nil {
|
||||
failed = true
|
||||
}
|
||||
}()
|
||||
actionResponse = fn()
|
||||
}()
|
||||
|
||||
if failed {
|
||||
if len(failure) > 0 {
|
||||
return failure[0]
|
||||
}
|
||||
return defaultFailureResponse
|
||||
}
|
||||
|
||||
return actionResponse
|
||||
}
|
||||
|
||||
const slashB byte = '/'
|
||||
|
||||
type compatibleErr interface {
|
||||
Error() string
|
||||
}
|
||||
|
||||
// DefaultErrStatusCode is the default error status code (400)
|
||||
// when the response contains an error which is not nil.
|
||||
var DefaultErrStatusCode = 400
|
||||
|
||||
// DispatchErr writes the error to the response.
|
||||
func DispatchErr(ctx context.Context, status int, err error) {
|
||||
if status < 400 {
|
||||
status = DefaultErrStatusCode
|
||||
}
|
||||
ctx.StatusCode(status)
|
||||
if text := err.Error(); text != "" {
|
||||
ctx.WriteString(text)
|
||||
ctx.StopExecution()
|
||||
}
|
||||
}
|
||||
|
||||
// DispatchCommon is being used internally to send
|
||||
// commonly used data to the response writer with a smart way.
|
||||
func DispatchCommon(ctx context.Context,
|
||||
statusCode int, contentType string, content []byte, v interface{}, err error, found bool) {
|
||||
|
||||
// if we have a false boolean as a return value
|
||||
// then skip everything and fire a not found,
|
||||
// we even don't care about the given status code or the object or the content.
|
||||
if !found {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
status := statusCode
|
||||
if status == 0 {
|
||||
status = 200
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
DispatchErr(ctx, status, err)
|
||||
return
|
||||
}
|
||||
|
||||
// write the status code, the rest will need that before any write ofc.
|
||||
ctx.StatusCode(status)
|
||||
if contentType == "" {
|
||||
// to respect any ctx.ContentType(...) call
|
||||
// especially if v is not nil.
|
||||
contentType = ctx.GetContentType()
|
||||
}
|
||||
|
||||
if v != nil {
|
||||
if d, ok := v.(Result); ok {
|
||||
// write the content type now (internal check for empty value)
|
||||
ctx.ContentType(contentType)
|
||||
d.Dispatch(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(contentType, context.ContentJavascriptHeaderValue) {
|
||||
_, err = ctx.JSONP(v)
|
||||
} else if strings.HasPrefix(contentType, context.ContentXMLHeaderValue) {
|
||||
_, err = ctx.XML(v, context.XML{Indent: " "})
|
||||
} else {
|
||||
// defaults to json if content type is missing or its application/json.
|
||||
_, err = ctx.JSON(v, context.JSON{Indent: " "})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
DispatchErr(ctx, status, err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx.ContentType(contentType)
|
||||
// .Write even len(content) == 0 , this should be called in order to call the internal tryWriteHeader,
|
||||
// it will not cost anything.
|
||||
ctx.Write(content)
|
||||
}
|
||||
|
||||
// DispatchFuncResult is being used internally to resolve
|
||||
// and send the method function's output values to the
|
||||
// context's response writer using a smart way which
|
||||
// respects status code, content type, content, custom struct
|
||||
// and an error type.
|
||||
// Supports for:
|
||||
// func(c *ExampleController) Get() string |
|
||||
// (string, string) |
|
||||
// (string, int) |
|
||||
// ...
|
||||
// int |
|
||||
// (int, string |
|
||||
// (string, error) |
|
||||
// ...
|
||||
// error |
|
||||
// (int, error) |
|
||||
// (customStruct, error) |
|
||||
// ...
|
||||
// bool |
|
||||
// (int, bool) |
|
||||
// (string, bool) |
|
||||
// (customStruct, bool) |
|
||||
// ...
|
||||
// customStruct |
|
||||
// (customStruct, int) |
|
||||
// (customStruct, string) |
|
||||
// Result or (Result, error) and so on...
|
||||
//
|
||||
// where Get is an HTTP METHOD.
|
||||
func DispatchFuncResult(ctx context.Context, values []reflect.Value) {
|
||||
numOut := len(values)
|
||||
if numOut == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
// if statusCode > 0 then send this status code.
|
||||
// Except when err != nil then check if status code is < 400 and
|
||||
// if it's set it as DefaultErrStatusCode.
|
||||
// Except when found == false, then the status code is 404.
|
||||
statusCode int
|
||||
// if not empty then use that as content type,
|
||||
// if empty and custom != nil then set it to application/json.
|
||||
contentType string
|
||||
// if len > 0 then write that to the response writer as raw bytes,
|
||||
// except when found == false or err != nil or custom != nil.
|
||||
content []byte
|
||||
// if not nil then check
|
||||
// for content type (or json default) and send the custom data object
|
||||
// except when found == false or err != nil.
|
||||
custom interface{}
|
||||
// if not nil then check for its status code,
|
||||
// if not status code or < 400 then set it as DefaultErrStatusCode
|
||||
// and fire the error's text.
|
||||
err error
|
||||
// if false then skip everything and fire 404.
|
||||
found = true // defaults to true of course, otherwise will break :)
|
||||
)
|
||||
|
||||
for _, v := range values {
|
||||
// order of these checks matters
|
||||
// for example, first we need to check for status code,
|
||||
// secondly the string (for content type and content)...
|
||||
if !v.IsValid() {
|
||||
continue
|
||||
}
|
||||
|
||||
f := v.Interface()
|
||||
|
||||
if b, ok := f.(bool); ok {
|
||||
found = b
|
||||
if !found {
|
||||
// skip everything, we don't care about other return values,
|
||||
// this boolean is the higher in order.
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if i, ok := f.(int); ok {
|
||||
statusCode = i
|
||||
continue
|
||||
}
|
||||
|
||||
if s, ok := f.(string); ok {
|
||||
// a string is content type when it contains a slash and
|
||||
// content or custom struct is being calculated already;
|
||||
// (string -> content, string-> content type)
|
||||
// (customStruct, string -> content type)
|
||||
if (len(content) > 0 || custom != nil) && strings.IndexByte(s, slashB) > 0 {
|
||||
contentType = s
|
||||
} else {
|
||||
// otherwise is content
|
||||
content = []byte(s)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if b, ok := f.([]byte); ok {
|
||||
// it's raw content, get the latest
|
||||
content = b
|
||||
continue
|
||||
}
|
||||
|
||||
if e, ok := f.(compatibleErr); ok {
|
||||
if e != nil { // it's always not nil but keep it here.
|
||||
err = e
|
||||
if statusCode < 400 {
|
||||
statusCode = DefaultErrStatusCode
|
||||
}
|
||||
break // break on first error, error should be in the end but we
|
||||
// need to know break the dispatcher if any error.
|
||||
// at the end; we don't want to write anything to the response if error is not nil.
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// else it's a custom struct or a dispatcher, we'll decide later
|
||||
// because content type and status code matters
|
||||
// do that check in order to be able to correctly dispatch:
|
||||
// (customStruct, error) -> customStruct filled and error is nil
|
||||
if custom == nil && f != nil {
|
||||
custom = f
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
DispatchCommon(ctx, statusCode, contentType, content, custom, err, found)
|
||||
}
|
||||
|
||||
// Response completes the `methodfunc.Result` interface.
|
||||
// It's being used as an alternative return value which
|
||||
// wraps the status code, the content type, a content as bytes or as string
|
||||
// and an error, it's smart enough to complete the request and send the correct response to the client.
|
||||
type Response struct {
|
||||
Code int
|
||||
ContentType string
|
||||
Content []byte
|
||||
|
||||
// if not empty then content type is the text/plain
|
||||
// and content is the text as []byte.
|
||||
Text string
|
||||
// If not nil then it will fire that as "application/json" or the
|
||||
// "ContentType" if not empty.
|
||||
Object interface{}
|
||||
|
||||
// If Path is not empty then it will redirect
|
||||
// the client to this Path, if Code is >= 300 and < 400
|
||||
// then it will use that Code to do the redirection, otherwise
|
||||
// StatusFound(302) or StatusSeeOther(303) for post methods will be used.
|
||||
// Except when err != nil.
|
||||
Path string
|
||||
|
||||
// if not empty then fire a 400 bad request error
|
||||
// unless the Status is > 200, then fire that error code
|
||||
// with the Err.Error() string as its content.
|
||||
//
|
||||
// if Err.Error() is empty then it fires the custom error handler
|
||||
// if any otherwise the framework sends the default http error text based on the status.
|
||||
Err error
|
||||
Try func() int
|
||||
|
||||
// if true then it skips everything else and it throws a 404 not found error.
|
||||
// Can be named as Failure but NotFound is more precise name in order
|
||||
// to be visible that it's different than the `Err`
|
||||
// because it throws a 404 not found instead of a 400 bad request.
|
||||
// NotFound bool
|
||||
// let's don't add this yet, it has its dangerous of missuse.
|
||||
}
|
||||
|
||||
var _ Result = Response{}
|
||||
|
||||
// Dispatch writes the response result to the context's response writer.
|
||||
func (r Response) Dispatch(ctx context.Context) {
|
||||
if r.Path != "" && r.Err == nil {
|
||||
// it's not a redirect valid status
|
||||
if r.Code < 300 || r.Code >= 400 {
|
||||
if ctx.Method() == "POST" {
|
||||
r.Code = 303 // StatusSeeOther
|
||||
}
|
||||
r.Code = 302 // StatusFound
|
||||
}
|
||||
ctx.Redirect(r.Path, r.Code)
|
||||
return
|
||||
}
|
||||
|
||||
if s := r.Text; s != "" {
|
||||
r.Content = []byte(s)
|
||||
}
|
||||
|
||||
DispatchCommon(ctx, r.Code, r.ContentType, r.Content, r.Object, r.Err, true)
|
||||
}
|
||||
|
||||
// View completes the `methodfunc.Result` interface.
|
||||
// It's being used as an alternative return value which
|
||||
// wraps the template file name, layout, (any) view data, status code and error.
|
||||
// It's smart enough to complete the request and send the correct response to the client.
|
||||
//
|
||||
// Example at: https://github.com/kataras/iris/blob/master/_examples/mvc/overview/web/controllers/hello_controller.go.
|
||||
type View struct {
|
||||
Name string
|
||||
Layout string
|
||||
Data interface{} // map or a custom struct.
|
||||
Code int
|
||||
Err error
|
||||
}
|
||||
|
||||
var _ Result = View{}
|
||||
|
||||
const dotB = byte('.')
|
||||
|
||||
// DefaultViewExt is the default extension if `view.Name `is missing,
|
||||
// but note that it doesn't care about
|
||||
// the app.RegisterView(iris.$VIEW_ENGINE("./$dir", "$ext"))'s $ext.
|
||||
// so if you don't use the ".html" as extension for your files
|
||||
// you have to append the extension manually into the `view.Name`
|
||||
// or change this global variable.
|
||||
var DefaultViewExt = ".html"
|
||||
|
||||
func ensureExt(s string) string {
|
||||
if len(s) == 0 {
|
||||
return "index" + DefaultViewExt
|
||||
}
|
||||
|
||||
if strings.IndexByte(s, dotB) < 1 {
|
||||
s += DefaultViewExt
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// Dispatch writes the template filename, template layout and (any) data to the client.
|
||||
// Completes the `Result` interface.
|
||||
func (r View) Dispatch(ctx context.Context) { // r as Response view.
|
||||
if r.Err != nil {
|
||||
if r.Code < 400 {
|
||||
r.Code = DefaultErrStatusCode
|
||||
}
|
||||
ctx.StatusCode(r.Code)
|
||||
ctx.WriteString(r.Err.Error())
|
||||
ctx.StopExecution()
|
||||
return
|
||||
}
|
||||
|
||||
if r.Code > 0 {
|
||||
ctx.StatusCode(r.Code)
|
||||
}
|
||||
|
||||
if r.Name != "" {
|
||||
r.Name = ensureExt(r.Name)
|
||||
|
||||
if r.Layout != "" {
|
||||
r.Layout = ensureExt(r.Layout)
|
||||
ctx.ViewLayout(r.Layout)
|
||||
}
|
||||
|
||||
if r.Data != nil {
|
||||
// In order to respect any c.Ctx.ViewData that may called manually before;
|
||||
dataKey := ctx.Application().ConfigurationReadOnly().GetViewDataContextKey()
|
||||
if ctx.Values().Get(dataKey) == nil {
|
||||
// if no c.Ctx.ViewData then it's empty do a
|
||||
// pure set, it's faster.
|
||||
ctx.Values().Set(dataKey, r.Data)
|
||||
} else {
|
||||
// else check if r.Data is map or struct, if struct convert it to map,
|
||||
// do a range loop and set the data one by one.
|
||||
// context.Map is actually a map[string]interface{} but we have to make that check;
|
||||
if m, ok := r.Data.(map[string]interface{}); ok {
|
||||
setViewData(ctx, m)
|
||||
} else if m, ok := r.Data.(context.Map); ok {
|
||||
setViewData(ctx, m)
|
||||
} else if structs.IsStruct(r.Data) {
|
||||
setViewData(ctx, structs.Map(r))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.View(r.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func setViewData(ctx context.Context, data map[string]interface{}) {
|
||||
for k, v := range data {
|
||||
ctx.ViewData(k, v)
|
||||
}
|
||||
}
|
271
mvc2/handler_out_test.go
Normal file
271
mvc2/handler_out_test.go
Normal file
|
@ -0,0 +1,271 @@
|
|||
package mvc2_test
|
||||
|
||||
// import (
|
||||
// "errors"
|
||||
// "testing"
|
||||
|
||||
// "github.com/kataras/iris"
|
||||
// "github.com/kataras/iris/context"
|
||||
// "github.com/kataras/iris/httptest"
|
||||
// "github.com/kataras/iris/mvc2"
|
||||
// )
|
||||
|
||||
// // activator/methodfunc/func_caller.go.
|
||||
// // and activator/methodfunc/func_result_dispatcher.go
|
||||
|
||||
// type testControllerMethodResult struct {
|
||||
// mvc2.C
|
||||
// }
|
||||
|
||||
// func (c *testControllerMethodResult) Get() mvc2.Result {
|
||||
// return mvc2.Response{
|
||||
// Text: "Hello World!",
|
||||
// }
|
||||
// }
|
||||
|
||||
// func (c *testControllerMethodResult) GetWithStatus() mvc2.Response { // or mvc.Result again, no problem.
|
||||
// return mvc2.Response{
|
||||
// Text: "This page doesn't exist",
|
||||
// Code: iris.StatusNotFound,
|
||||
// }
|
||||
// }
|
||||
|
||||
// type testCustomStruct struct {
|
||||
// Name string `json:"name" xml:"name"`
|
||||
// Age int `json:"age" xml:"age"`
|
||||
// }
|
||||
|
||||
// func (c *testControllerMethodResult) GetJson() mvc2.Result {
|
||||
// var err error
|
||||
// if c.Ctx.URLParamExists("err") {
|
||||
// err = errors.New("error here")
|
||||
// }
|
||||
// return mvc2.Response{
|
||||
// Err: err, // if err != nil then it will fire the error's text with a BadRequest.
|
||||
// Object: testCustomStruct{Name: "Iris", Age: 2},
|
||||
// }
|
||||
// }
|
||||
|
||||
// var things = []string{"thing 0", "thing 1", "thing 2"}
|
||||
|
||||
// func (c *testControllerMethodResult) GetThingWithTryBy(index int) mvc2.Result {
|
||||
// failure := mvc2.Response{
|
||||
// Text: "thing does not exist",
|
||||
// Code: iris.StatusNotFound,
|
||||
// }
|
||||
|
||||
// return mvc2.Try(func() mvc2.Result {
|
||||
// // if panic because of index exceed the slice
|
||||
// // then the "failure" response will be returned instead.
|
||||
// return mvc2.Response{Text: things[index]}
|
||||
// }, failure)
|
||||
// }
|
||||
|
||||
// func (c *testControllerMethodResult) GetThingWithTryDefaultBy(index int) mvc2.Result {
|
||||
// return mvc2.Try(func() mvc2.Result {
|
||||
// // if panic because of index exceed the slice
|
||||
// // then the default failure response will be returned instead (400 bad request).
|
||||
// return mvc2.Response{Text: things[index]}
|
||||
// })
|
||||
// }
|
||||
|
||||
// func TestControllerMethodResult(t *testing.T) {
|
||||
// app := iris.New()
|
||||
// app.Controller("/", new(testControllerMethodResult))
|
||||
|
||||
// e := httptest.New(t, app)
|
||||
|
||||
// e.GET("/").Expect().Status(iris.StatusOK).
|
||||
// Body().Equal("Hello World!")
|
||||
|
||||
// e.GET("/with/status").Expect().Status(iris.StatusNotFound).
|
||||
// Body().Equal("This page doesn't exist")
|
||||
|
||||
// e.GET("/json").Expect().Status(iris.StatusOK).
|
||||
// JSON().Equal(iris.Map{
|
||||
// "name": "Iris",
|
||||
// "age": 2,
|
||||
// })
|
||||
|
||||
// e.GET("/json").WithQuery("err", true).Expect().
|
||||
// Status(iris.StatusBadRequest).
|
||||
// Body().Equal("error here")
|
||||
|
||||
// e.GET("/thing/with/try/1").Expect().
|
||||
// Status(iris.StatusOK).
|
||||
// Body().Equal("thing 1")
|
||||
// // failure because of index exceed the slice
|
||||
// e.GET("/thing/with/try/3").Expect().
|
||||
// Status(iris.StatusNotFound).
|
||||
// Body().Equal("thing does not exist")
|
||||
|
||||
// e.GET("/thing/with/try/default/3").Expect().
|
||||
// Status(iris.StatusBadRequest).
|
||||
// Body().Equal("Bad Request")
|
||||
// }
|
||||
|
||||
// type testControllerMethodResultTypes struct {
|
||||
// mvc2.C
|
||||
// }
|
||||
|
||||
// func (c *testControllerMethodResultTypes) GetText() string {
|
||||
// return "text"
|
||||
// }
|
||||
|
||||
// func (c *testControllerMethodResultTypes) GetStatus() int {
|
||||
// return iris.StatusBadGateway
|
||||
// }
|
||||
|
||||
// func (c *testControllerMethodResultTypes) GetTextWithStatusOk() (string, int) {
|
||||
// return "OK", iris.StatusOK
|
||||
// }
|
||||
|
||||
// // tests should have output arguments mixed
|
||||
// func (c *testControllerMethodResultTypes) GetStatusWithTextNotOkBy(first string, second string) (int, string) {
|
||||
// return iris.StatusForbidden, "NOT_OK_" + first + second
|
||||
// }
|
||||
|
||||
// func (c *testControllerMethodResultTypes) GetTextAndContentType() (string, string) {
|
||||
// return "<b>text</b>", "text/html"
|
||||
// }
|
||||
|
||||
// type testControllerMethodCustomResult struct {
|
||||
// HTML string
|
||||
// }
|
||||
|
||||
// // The only one required function to make that a custom Response dispatcher.
|
||||
// func (r testControllerMethodCustomResult) Dispatch(ctx context.Context) {
|
||||
// ctx.HTML(r.HTML)
|
||||
// }
|
||||
|
||||
// func (c *testControllerMethodResultTypes) GetCustomResponse() testControllerMethodCustomResult {
|
||||
// return testControllerMethodCustomResult{"<b>text</b>"}
|
||||
// }
|
||||
|
||||
// func (c *testControllerMethodResultTypes) GetCustomResponseWithStatusOk() (testControllerMethodCustomResult, int) {
|
||||
// return testControllerMethodCustomResult{"<b>OK</b>"}, iris.StatusOK
|
||||
// }
|
||||
|
||||
// func (c *testControllerMethodResultTypes) GetCustomResponseWithStatusNotOk() (testControllerMethodCustomResult, int) {
|
||||
// return testControllerMethodCustomResult{"<b>internal server error</b>"}, iris.StatusInternalServerError
|
||||
// }
|
||||
|
||||
// func (c *testControllerMethodResultTypes) GetCustomStruct() testCustomStruct {
|
||||
// return testCustomStruct{"Iris", 2}
|
||||
// }
|
||||
|
||||
// func (c *testControllerMethodResultTypes) GetCustomStructWithStatusNotOk() (testCustomStruct, int) {
|
||||
// return testCustomStruct{"Iris", 2}, iris.StatusInternalServerError
|
||||
// }
|
||||
|
||||
// func (c *testControllerMethodResultTypes) GetCustomStructWithContentType() (testCustomStruct, string) {
|
||||
// return testCustomStruct{"Iris", 2}, "text/xml"
|
||||
// }
|
||||
|
||||
// func (c *testControllerMethodResultTypes) GetCustomStructWithError() (s testCustomStruct, err error) {
|
||||
// s = testCustomStruct{"Iris", 2}
|
||||
// if c.Ctx.URLParamExists("err") {
|
||||
// err = errors.New("omit return of testCustomStruct and fire error")
|
||||
// }
|
||||
|
||||
// // it should send the testCustomStruct as JSON if error is nil
|
||||
// // otherwise it should fire the default error(BadRequest) with the error's text.
|
||||
// return
|
||||
// }
|
||||
|
||||
// func TestControllerMethodResultTypes(t *testing.T) {
|
||||
// app := iris.New()
|
||||
// app.Controller("/", new(testControllerMethodResultTypes))
|
||||
|
||||
// e := httptest.New(t, app)
|
||||
|
||||
// e.GET("/text").Expect().Status(iris.StatusOK).
|
||||
// Body().Equal("text")
|
||||
|
||||
// e.GET("/status").Expect().Status(iris.StatusBadGateway)
|
||||
|
||||
// e.GET("/text/with/status/ok").Expect().Status(iris.StatusOK).
|
||||
// Body().Equal("OK")
|
||||
|
||||
// e.GET("/status/with/text/not/ok/first/second").Expect().Status(iris.StatusForbidden).
|
||||
// Body().Equal("NOT_OK_firstsecond")
|
||||
|
||||
// e.GET("/text/and/content/type").Expect().Status(iris.StatusOK).
|
||||
// ContentType("text/html", "utf-8").
|
||||
// Body().Equal("<b>text</b>")
|
||||
|
||||
// e.GET("/custom/response").Expect().Status(iris.StatusOK).
|
||||
// ContentType("text/html", "utf-8").
|
||||
// Body().Equal("<b>text</b>")
|
||||
// e.GET("/custom/response/with/status/ok").Expect().Status(iris.StatusOK).
|
||||
// ContentType("text/html", "utf-8").
|
||||
// Body().Equal("<b>OK</b>")
|
||||
// e.GET("/custom/response/with/status/not/ok").Expect().Status(iris.StatusInternalServerError).
|
||||
// ContentType("text/html", "utf-8").
|
||||
// Body().Equal("<b>internal server error</b>")
|
||||
|
||||
// expectedResultFromCustomStruct := map[string]interface{}{
|
||||
// "name": "Iris",
|
||||
// "age": 2,
|
||||
// }
|
||||
// e.GET("/custom/struct").Expect().Status(iris.StatusOK).
|
||||
// JSON().Equal(expectedResultFromCustomStruct)
|
||||
// e.GET("/custom/struct/with/status/not/ok").Expect().Status(iris.StatusInternalServerError).
|
||||
// JSON().Equal(expectedResultFromCustomStruct)
|
||||
// e.GET("/custom/struct/with/content/type").Expect().Status(iris.StatusOK).
|
||||
// ContentType("text/xml", "utf-8")
|
||||
// e.GET("/custom/struct/with/error").Expect().Status(iris.StatusOK).
|
||||
// JSON().Equal(expectedResultFromCustomStruct)
|
||||
// e.GET("/custom/struct/with/error").WithQuery("err", true).Expect().
|
||||
// Status(iris.StatusBadRequest). // the default status code if error is not nil
|
||||
// // the content should be not JSON it should be the status code's text
|
||||
// // it will fire the error's text
|
||||
// Body().Equal("omit return of testCustomStruct and fire error")
|
||||
// }
|
||||
|
||||
// type testControllerViewResultRespectCtxViewData struct {
|
||||
// T *testing.T
|
||||
// mvc2.C
|
||||
// }
|
||||
|
||||
// func (t *testControllerViewResultRespectCtxViewData) BeginRequest(ctx context.Context) {
|
||||
// t.C.BeginRequest(ctx)
|
||||
// ctx.ViewData("name_begin", "iris_begin")
|
||||
// }
|
||||
|
||||
// func (t *testControllerViewResultRespectCtxViewData) EndRequest(ctx context.Context) {
|
||||
// t.C.EndRequest(ctx)
|
||||
// // check if data is not overridden by return mvc.View {Data: context.Map...}
|
||||
|
||||
// dataWritten := ctx.GetViewData()
|
||||
// if dataWritten == nil {
|
||||
// t.T.Fatalf("view data is nil, both BeginRequest and Get failed to write the data")
|
||||
// return
|
||||
// }
|
||||
|
||||
// if dataWritten["name_begin"] == nil {
|
||||
// t.T.Fatalf(`view data[name_begin] is nil,
|
||||
// BeginRequest's ctx.ViewData call have been overridden by Get's return mvc.View {Data: }.
|
||||
// Total view data: %v`, dataWritten)
|
||||
// }
|
||||
|
||||
// if dataWritten["name"] == nil {
|
||||
// t.T.Fatalf("view data[name] is nil, Get's return mvc.View {Data: } didn't work. Total view data: %v", dataWritten)
|
||||
// }
|
||||
// }
|
||||
|
||||
// func (t *testControllerViewResultRespectCtxViewData) Get() mvc2.Result {
|
||||
// return mvc2.View{
|
||||
// Name: "doesnt_exists.html",
|
||||
// Data: context.Map{"name": "iris"}, // we care about this only.
|
||||
// Code: iris.StatusInternalServerError,
|
||||
// }
|
||||
// }
|
||||
|
||||
// func TestControllerViewResultRespectCtxViewData(t *testing.T) {
|
||||
// app := iris.New()
|
||||
// app.Controller("/", new(testControllerViewResultRespectCtxViewData), t)
|
||||
// e := httptest.New(t, app)
|
||||
|
||||
// e.GET("/").Expect().Status(iris.StatusInternalServerError)
|
||||
// }
|
47
mvc2/session_controller.go
Normal file
47
mvc2/session_controller.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package mvc2
|
||||
|
||||
import (
|
||||
"github.com/kataras/iris/context"
|
||||
"github.com/kataras/iris/sessions"
|
||||
"reflect"
|
||||
|
||||
"github.com/kataras/golog"
|
||||
)
|
||||
|
||||
var defaultManager = sessions.New(sessions.Config{})
|
||||
|
||||
// SessionController is a simple `Controller` implementation
|
||||
// which requires a binded session manager in order to give
|
||||
// direct access to the current client's session via its `Session` field.
|
||||
type SessionController struct {
|
||||
C
|
||||
|
||||
Manager *sessions.Sessions
|
||||
Session *sessions.Session
|
||||
}
|
||||
|
||||
// OnActivate called, once per application lifecycle NOT request,
|
||||
// every single time the dev registers a specific SessionController-based controller.
|
||||
// It makes sure that its "Manager" field is filled
|
||||
// even if the caller didn't provide any sessions manager via the `app.Controller` function.
|
||||
func (s *SessionController) OnActivate(ca *ControllerActivator) {
|
||||
if !ca.Engine.BindTypeExists(reflect.TypeOf(defaultManager)) {
|
||||
ca.Engine.Bind(defaultManager)
|
||||
golog.Warnf(`MVC SessionController: couldn't find any "*sessions.Sessions" bindable value to fill the "Manager" field,
|
||||
therefore this controller is using the default sessions manager instead.
|
||||
Please refer to the documentation to learn how you can provide the session manager`)
|
||||
}
|
||||
}
|
||||
|
||||
// BeginRequest calls the Controller's BeginRequest
|
||||
// and tries to initialize the current user's Session.
|
||||
func (s *SessionController) BeginRequest(ctx context.Context) {
|
||||
s.C.BeginRequest(ctx)
|
||||
if s.Manager == nil {
|
||||
ctx.Application().Logger().Errorf(`MVC SessionController: sessions manager is nil, report this as a bug
|
||||
because the SessionController should predict this on its activation state and use a default one automatically`)
|
||||
return
|
||||
}
|
||||
|
||||
s.Session = s.Manager.Start(ctx)
|
||||
}
|
Loading…
Reference in New Issue
Block a user