From a7b2a90e3bbb94a33672a1c57e05e1c33715558d Mon Sep 17 00:00:00 2001 From: kataras Date: Mon, 4 Dec 2017 05:08:05 +0200 Subject: [PATCH] cm Former-commit-id: 8f99121b81dc76c04d5910117885d9286873f26c --- mvc2/binder/binder/input.go | 4 + mvc2/binder/binding.go | 19 ++ mvc2/binder/func_input.go | 1 + mvc2/binder/func_result.go | 53 ++++ mvc2/binder/reflect.go | 107 ++++++++ mvc2/binder/to_struct.go | 50 ++++ mvc2/binder_in.go | 173 ++++++++++++ mvc2/binder_in_path_param.go | 135 ++++++++++ mvc2/binder_in_path_param_test.go | 64 +++++ mvc2/binder_in_service.go | 81 ++++++ mvc2/binder_in_service_test.go | 46 ++++ mvc2/binder_in_test.go | 143 ++++++++++ mvc2/engine.go | 103 ++++++++ mvc2/handler_out.go | 422 ++++++++++++++++++++++++++++++ mvc2/handler_out_test.go | 271 +++++++++++++++++++ mvc2/session_controller.go | 47 ++++ 16 files changed, 1719 insertions(+) create mode 100644 mvc2/binder/binder/input.go create mode 100644 mvc2/binder/binding.go create mode 100644 mvc2/binder/func_input.go create mode 100644 mvc2/binder/func_result.go create mode 100644 mvc2/binder/reflect.go create mode 100644 mvc2/binder/to_struct.go create mode 100644 mvc2/binder_in.go create mode 100644 mvc2/binder_in_path_param.go create mode 100644 mvc2/binder_in_path_param_test.go create mode 100644 mvc2/binder_in_service.go create mode 100644 mvc2/binder_in_service_test.go create mode 100644 mvc2/binder_in_test.go create mode 100644 mvc2/engine.go create mode 100644 mvc2/handler_out.go create mode 100644 mvc2/handler_out_test.go create mode 100644 mvc2/session_controller.go diff --git a/mvc2/binder/binder/input.go b/mvc2/binder/binder/input.go new file mode 100644 index 00000000..1e583530 --- /dev/null +++ b/mvc2/binder/binder/input.go @@ -0,0 +1,4 @@ +package binder + +type Input interface { +} diff --git a/mvc2/binder/binding.go b/mvc2/binder/binding.go new file mode 100644 index 00000000..9fe0eed3 --- /dev/null +++ b/mvc2/binder/binding.go @@ -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 +} diff --git a/mvc2/binder/func_input.go b/mvc2/binder/func_input.go new file mode 100644 index 00000000..0587a0cc --- /dev/null +++ b/mvc2/binder/func_input.go @@ -0,0 +1 @@ +package binder diff --git a/mvc2/binder/func_result.go b/mvc2/binder/func_result.go new file mode 100644 index 00000000..cb8ba5fe --- /dev/null +++ b/mvc2/binder/func_result.go @@ -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 +} diff --git a/mvc2/binder/reflect.go b/mvc2/binder/reflect.go new file mode 100644 index 00000000..20c75b9f --- /dev/null +++ b/mvc2/binder/reflect.go @@ -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 +} diff --git a/mvc2/binder/to_struct.go b/mvc2/binder/to_struct.go new file mode 100644 index 00000000..cb85d0d6 --- /dev/null +++ b/mvc2/binder/to_struct.go @@ -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 +} diff --git a/mvc2/binder_in.go b/mvc2/binder_in.go new file mode 100644 index 00000000..acc8dee8 --- /dev/null +++ b/mvc2/binder_in.go @@ -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 +} diff --git a/mvc2/binder_in_path_param.go b/mvc2/binder_in_path_param.go new file mode 100644 index 00000000..52196564 --- /dev/null +++ b/mvc2/binder_in_path_param.go @@ -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} + } +} diff --git a/mvc2/binder_in_path_param_test.go b/mvc2/binder_in_path_param_test.go new file mode 100644 index 00000000..582a1ee2 --- /dev/null +++ b/mvc2/binder_in_path_param_test.go @@ -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") + } +} diff --git a/mvc2/binder_in_service.go b/mvc2/binder_in_service.go new file mode 100644 index 00000000..db4fde08 --- /dev/null +++ b/mvc2/binder_in_service.go @@ -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 +} diff --git a/mvc2/binder_in_service_test.go b/mvc2/binder_in_service_test.go new file mode 100644 index 00000000..88779dce --- /dev/null +++ b/mvc2/binder_in_service_test.go @@ -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{}{})) + +} diff --git a/mvc2/binder_in_test.go b/mvc2/binder_in_test.go new file mode 100644 index 00000000..099a3578 --- /dev/null +++ b/mvc2/binder_in_test.go @@ -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 "" +} diff --git a/mvc2/engine.go b/mvc2/engine.go new file mode 100644 index 00000000..f40816e0 --- /dev/null +++ b/mvc2/engine.go @@ -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) + } +} diff --git a/mvc2/handler_out.go b/mvc2/handler_out.go new file mode 100644 index 00000000..eb7999ad --- /dev/null +++ b/mvc2/handler_out.go @@ -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) + } +} diff --git a/mvc2/handler_out_test.go b/mvc2/handler_out_test.go new file mode 100644 index 00000000..bf5abda3 --- /dev/null +++ b/mvc2/handler_out_test.go @@ -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 "text", "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{"text"} +// } + +// func (c *testControllerMethodResultTypes) GetCustomResponseWithStatusOk() (testControllerMethodCustomResult, int) { +// return testControllerMethodCustomResult{"OK"}, iris.StatusOK +// } + +// func (c *testControllerMethodResultTypes) GetCustomResponseWithStatusNotOk() (testControllerMethodCustomResult, int) { +// return testControllerMethodCustomResult{"internal server error"}, 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("text") + +// e.GET("/custom/response").Expect().Status(iris.StatusOK). +// ContentType("text/html", "utf-8"). +// Body().Equal("text") +// e.GET("/custom/response/with/status/ok").Expect().Status(iris.StatusOK). +// ContentType("text/html", "utf-8"). +// Body().Equal("OK") +// e.GET("/custom/response/with/status/not/ok").Expect().Status(iris.StatusInternalServerError). +// ContentType("text/html", "utf-8"). +// Body().Equal("internal server error") + +// 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) +// } diff --git a/mvc2/session_controller.go b/mvc2/session_controller.go new file mode 100644 index 00000000..5ea5f5c4 --- /dev/null +++ b/mvc2/session_controller.go @@ -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) +}