Former-commit-id: 8f99121b81dc76c04d5910117885d9286873f26c
This commit is contained in:
kataras 2017-12-04 05:08:05 +02:00
parent 7043f352d9
commit a7b2a90e3b
16 changed files with 1719 additions and 0 deletions

View File

@ -0,0 +1,4 @@
package binder
type Input interface {
}

19
mvc2/binder/binding.go Normal file
View 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
}

View File

@ -0,0 +1 @@
package binder

View 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
View 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
View 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
View 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
}

View 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}
}
}

View 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
View 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
}

View 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
View 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
View 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
View 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
View 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)
// }

View 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)
}