mirror of
https://github.com/kataras/iris.git
synced 2025-12-18 18:37:05 +00:00
OK, my dream-idea is implemented. TODO: Some examples and doc.go is not updated yet, comments on the mvc/di subpackage, the tutorial/vuejs-todo-mvc is running but not finished yet (it's using browser's localstorage and it should be replaced by the http requests that are registered via iris mvc
Former-commit-id: 0ea7e01ce1d78bcb78b40f3b0f5c03ad7c9abaea
This commit is contained in:
4
mvc/AUTHORS
Normal file
4
mvc/AUTHORS
Normal file
@@ -0,0 +1,4 @@
|
||||
# This is the official list of Iris MVC authors for copyright
|
||||
# purposes.
|
||||
|
||||
Gerasimos Maropoulos <kataras2006@hotmail.com>
|
||||
27
mvc/LICENSE
Normal file
27
mvc/LICENSE
Normal file
@@ -0,0 +1,27 @@
|
||||
Copyright (c) 2018 Gerasimos Maropoulos. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Iris nor the names of its
|
||||
contributor, Gerasimos Maropoulos, may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
118
mvc/README.md
Normal file
118
mvc/README.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# MVC Internals
|
||||
|
||||
* `MakeHandler` - accepts a function which accepts any input and outputs any result, and any optional values that will be used as binders, if needed they will be converted in order to be faster at serve-time. Returns a `context/iris#Handler` and a non-nil error if passed function cannot be wrapped to a raw `context/iris#Handler`
|
||||
* `Engine` - The "manager" of the controllers and handlers, can be grouped and an `Engine` can have any number of children.
|
||||
* `Engine#Bind` Binds values to be used inside on one or more handlers and controllers
|
||||
* `Engine#Handler` - Creates and returns a new mvc handler, which accept any input parameters (calculated by the binders) and output any result which will be sent as a response to the HTTP Client. Calls the `MakeHandler` with the Engine's `Input` values as the binders
|
||||
* `Engine#Controller` - Creates and activates a controller based on a struct which has the `C` as an embedded , anonymous, field and defines methods to be used as routes. Can accept any optional activator listeners in order to bind any custom routes or change the bindings, called once at startup
|
||||
* `C`
|
||||
* Struct fields with `Struct Binding`
|
||||
* Methods with `Dynamic Binding`
|
||||
|
||||
|
||||
Examples can be found at: https://github.com/kataras/iris/tree/master/_examples/mvc.
|
||||
|
||||
## Binding
|
||||
|
||||
First of all, they can be binded to `func input arguments` (custom handlers) or `struct fields` (controllers). We will use the term `Input` for both of them.
|
||||
|
||||
```go
|
||||
// consume the user here as func input argument.
|
||||
func myHandler(user User) {}
|
||||
|
||||
type myController struct {
|
||||
C
|
||||
|
||||
// consume the user here, as struct field.
|
||||
user User
|
||||
}
|
||||
```
|
||||
|
||||
If the input is an interface then it will check if the binding is completed this interface
|
||||
and it will be binded as expected.
|
||||
|
||||
Two types of binders are supported:
|
||||
|
||||
### Dynamic Binding
|
||||
|
||||
`ReturnValue`, should return a single value, no pointer to, if the consumer Input (`struct field` or `func input argument`) expects `User` then it will be binded on each request, this is a dynamic binding based on the `Context`.
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
Username string
|
||||
}
|
||||
|
||||
myBinder := func(ctx iris.Context) User {
|
||||
return User {
|
||||
Username: ctx.Params().Get("username"),
|
||||
}
|
||||
}
|
||||
|
||||
myHandler := func(user User) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Static Binding
|
||||
|
||||
`Value (Service)`, this is used to bind a value instance, like a service or a database connection.
|
||||
|
||||
```go
|
||||
// optional but we declare interface most of the times to
|
||||
// be easier to switch from production to testing or local and visa versa.
|
||||
// If input is an interface then it will check if the binding is completed this interface
|
||||
// and it will be binded as expected.
|
||||
type Service interface {
|
||||
Do() string
|
||||
}
|
||||
|
||||
type myProductionService struct {
|
||||
text string
|
||||
}
|
||||
func (s *myProductionService) Do() string {
|
||||
return s.text
|
||||
}
|
||||
|
||||
myService := &myProductionService{text: "something"}
|
||||
|
||||
myHandler := func(service Service) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Bind
|
||||
|
||||
#### For Handlers
|
||||
|
||||
MakeHandler is used to create a handler based on a function which can accept any input arguments and export any output arguments, the input arguments can be dynamic path parameters or custom [binders](#binding).
|
||||
|
||||
```go
|
||||
h, err := MakeHandler(myHandler, reflect.ValueOf(myBinder))
|
||||
```
|
||||
|
||||
Values passed in `Bind` are binded to all handlers and controllers that are expected a type of the returned value, in this case the myBinder indicates a dynamic/serve-time function which returns a User, as shown above.
|
||||
|
||||
```go
|
||||
m := New().Bind(myBinder)
|
||||
|
||||
h := m.Handler(myHandler)
|
||||
```
|
||||
|
||||
#### For Controllers
|
||||
|
||||
```go
|
||||
app := iris.New()
|
||||
New().Bind(myBinder).Controller(app, new(myController))
|
||||
// ...
|
||||
```
|
||||
|
||||
```go
|
||||
sub := app.Party("/sub")
|
||||
New().Controller(sub, &myController{service: myService})
|
||||
```
|
||||
|
||||
```go
|
||||
New().Controller(sub.Party("/subsub"), new(myController), func(ca *ControllerActivator) {
|
||||
ca.Bind(myService)
|
||||
})
|
||||
```
|
||||
290
mvc/controller.go
Normal file
290
mvc/controller.go
Normal file
@@ -0,0 +1,290 @@
|
||||
package mvc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/kataras/iris/mvc/di"
|
||||
|
||||
"github.com/kataras/iris/context"
|
||||
"github.com/kataras/iris/core/router"
|
||||
"github.com/kataras/iris/core/router/macro"
|
||||
)
|
||||
|
||||
// BaseController is the optional controller interface, if it's
|
||||
// completed by the end controller then the BeginRequest and EndRequest
|
||||
// are called between the controller's method responsible for the incoming request.
|
||||
type BaseController interface {
|
||||
BeginRequest(context.Context)
|
||||
EndRequest(context.Context)
|
||||
}
|
||||
|
||||
// ControllerActivator returns a new controller type info description.
|
||||
// Its functionality can be overriden by the end-dev.
|
||||
type ControllerActivator struct {
|
||||
// the router is used on the `Activate` and can be used by end-dev on the `BeforeActivate`
|
||||
// to register any custom controller's functions as handlers but we will need it here
|
||||
// in order to not create a new type like `ActivationPayload` for the `BeforeActivate`.
|
||||
Router router.Party
|
||||
|
||||
// initRef BaseController // the BaseController as it's passed from the end-dev.
|
||||
Value reflect.Value // the BaseController's Value.
|
||||
Type reflect.Type // raw type of the BaseController (initRef).
|
||||
// FullName it's the last package path segment + "." + the Name.
|
||||
// i.e: if login-example/user/controller.go, the FullName is "user.Controller".
|
||||
FullName string
|
||||
|
||||
// the methods names that is already binded to a handler,
|
||||
// the BeginRequest, EndRequest and BeforeActivate are reserved by the internal implementation.
|
||||
reservedMethods []string
|
||||
|
||||
// the bindings that comes from the Engine and the controller's filled fields if any.
|
||||
// Can be binded to the the new controller's fields and method that is fired
|
||||
// on incoming requests.
|
||||
Dependencies *di.D
|
||||
|
||||
// on activate.
|
||||
injector *di.StructInjector
|
||||
}
|
||||
|
||||
func getNameOf(typ reflect.Type) string {
|
||||
elemTyp := di.IndirectType(typ)
|
||||
|
||||
typName := elemTyp.Name()
|
||||
pkgPath := elemTyp.PkgPath()
|
||||
fullname := pkgPath[strings.LastIndexByte(pkgPath, '/')+1:] + "." + typName
|
||||
|
||||
return fullname
|
||||
}
|
||||
|
||||
func newControllerActivator(router router.Party, controller interface{}, d *di.D) *ControllerActivator {
|
||||
var (
|
||||
val = reflect.ValueOf(controller)
|
||||
typ = val.Type()
|
||||
|
||||
// the full name of the controller, it's its type including the package path.
|
||||
fullName = getNameOf(typ)
|
||||
)
|
||||
|
||||
// the following will make sure that if
|
||||
// the controller's has set-ed pointer struct fields by the end-dev
|
||||
// we will include them to the bindings.
|
||||
// set bindings to the non-zero pointer fields' values that may be set-ed by
|
||||
// the end-developer when declaring the controller,
|
||||
// activate listeners needs them in order to know if something set-ed already or not,
|
||||
// look `BindTypeExists`.
|
||||
d.Values = append(di.LookupNonZeroFieldsValues(val), d.Values...)
|
||||
|
||||
c := &ControllerActivator{
|
||||
// give access to the Router to the end-devs if they need it for some reason,
|
||||
// i.e register done handlers.
|
||||
Router: router,
|
||||
Value: val,
|
||||
Type: typ,
|
||||
FullName: fullName,
|
||||
// set some methods that end-dev cann't use accidentally
|
||||
// to register a route via the `Handle`,
|
||||
// all available exported and compatible methods
|
||||
// are being appended to the slice at the `parseMethods`,
|
||||
// if a new method is registered via `Handle` its function name
|
||||
// is also appended to that slice.
|
||||
//
|
||||
// TODO: now that BaseController is totally optionally
|
||||
// we have to check if BeginRequest and EndRequest should be here.
|
||||
reservedMethods: whatReservedMethods(typ),
|
||||
Dependencies: d,
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func whatReservedMethods(typ reflect.Type) []string {
|
||||
methods := []string{"BeforeActivate"}
|
||||
if isBaseController(typ) {
|
||||
methods = append(methods, "BeginRequest", "EndRequest")
|
||||
}
|
||||
|
||||
return methods
|
||||
}
|
||||
|
||||
// checks if a method is already registered.
|
||||
func (c *ControllerActivator) isReservedMethod(name string) bool {
|
||||
for _, s := range c.reservedMethods {
|
||||
if s == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// register all available, exported methods to handlers if possible.
|
||||
func (c *ControllerActivator) parseMethods() {
|
||||
n := c.Type.NumMethod()
|
||||
for i := 0; i < n; i++ {
|
||||
m := c.Type.Method(i)
|
||||
|
||||
httpMethod, httpPath, err := parseMethod(m, c.isReservedMethod)
|
||||
if err != nil {
|
||||
if err != errSkip {
|
||||
err = fmt.Errorf("MVC: fail to parse the route path and HTTP method for '%s.%s': %v", c.FullName, m.Name, err)
|
||||
c.Router.GetReporter().AddErr(err)
|
||||
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
c.Handle(httpMethod, httpPath, m.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ControllerActivator) activate() {
|
||||
c.parseMethods()
|
||||
}
|
||||
|
||||
var emptyIn = []reflect.Value{}
|
||||
|
||||
// Handle registers a route based on a http method, the route's path
|
||||
// and a function name that belongs to the controller, it accepts
|
||||
// a forth, optionally, variadic parameter which is the before handlers.
|
||||
//
|
||||
// Just like `APIBuilder`, it returns the `*router.Route`, if failed
|
||||
// then it logs the errors and it returns nil, you can check the errors
|
||||
// programmatically by the `APIBuilder#GetReporter`.
|
||||
func (c *ControllerActivator) Handle(method, path, funcName string, middleware ...context.Handler) *router.Route {
|
||||
if method == "" || path == "" || funcName == "" ||
|
||||
c.isReservedMethod(funcName) {
|
||||
// isReservedMethod -> if it's already registered
|
||||
// by a previous Handle or analyze methods internally.
|
||||
return nil
|
||||
}
|
||||
|
||||
// get the method from the controller type.
|
||||
m, ok := c.Type.MethodByName(funcName)
|
||||
if !ok {
|
||||
err := fmt.Errorf("MVC: function '%s' doesn't exist inside the '%s' controller",
|
||||
funcName, c.FullName)
|
||||
c.Router.GetReporter().AddErr(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// parse a route template which contains the parameters organised.
|
||||
tmpl, err := macro.Parse(path, c.Router.Macros())
|
||||
if err != nil {
|
||||
err = fmt.Errorf("MVC: fail to parse the path for '%s.%s': %v", c.FullName, funcName, err)
|
||||
c.Router.GetReporter().AddErr(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// add this as a reserved method name in order to
|
||||
// be sure that the same func will not be registered again, even if a custom .Handle later on.
|
||||
c.reservedMethods = append(c.reservedMethods, funcName)
|
||||
|
||||
// get the function's input.
|
||||
funcIn := getInputArgsFromFunc(m.Type)
|
||||
|
||||
// get the path parameters bindings from the template,
|
||||
// use the function's input except the receiver which is the
|
||||
// end-dev's controller pointer.
|
||||
pathParams := getPathParamsForInput(tmpl.Params, funcIn[1:]...)
|
||||
// get the function's input arguments' bindings.
|
||||
funcDependencies := c.Dependencies.Clone()
|
||||
funcDependencies.AddValue(pathParams...)
|
||||
|
||||
// fmt.Printf("for %s | values: %s\n", funcName, funcDependencies.Values)
|
||||
funcInjector := funcDependencies.Func(m.Func)
|
||||
// fmt.Printf("actual injector's inputs length: %d\n", funcInjector.Length)
|
||||
|
||||
// the element value, not the pointer, wil lbe used to create a
|
||||
// new controller on each incoming request.
|
||||
|
||||
// Remember:
|
||||
// we cannot simply do that and expect to work:
|
||||
// hasStructInjector = c.injector != nil && c.injector.Valid
|
||||
// hasFuncInjector = funcInjector != nil && funcInjector.Valid
|
||||
// because
|
||||
// the `Handle` can be called from `BeforeActivate` callbacks
|
||||
// and before activation, the c.injector is nil because
|
||||
// we may not have the dependencies binded yet. But if `c.injector.Valid`
|
||||
// inside the Handelr works because it's set on the `activate()` method.
|
||||
// To solve this we can make check on the FIRST `Handle`,
|
||||
// if c.injector is nil, then set it with the current bindings,
|
||||
// so the user should bind the dependencies needed before the `Handle`
|
||||
// this is a logical flow, so we will choose that one ->
|
||||
if c.injector == nil {
|
||||
c.injector = c.Dependencies.Struct(c.Value)
|
||||
}
|
||||
var (
|
||||
hasStructInjector = c.injector != nil && c.injector.Valid
|
||||
hasFuncInjector = funcInjector != nil && funcInjector.Valid
|
||||
|
||||
implementsBase = isBaseController(c.Type)
|
||||
// we will make use of 'n' to make a slice of reflect.Value
|
||||
// to pass into if the function has input arguments that
|
||||
// are will being filled by the funcDependencies.
|
||||
n = len(funcIn)
|
||||
|
||||
elemTyp = di.IndirectType(c.Type)
|
||||
)
|
||||
|
||||
handler := func(ctx context.Context) {
|
||||
ctrl := reflect.New(elemTyp)
|
||||
|
||||
if implementsBase {
|
||||
// the Interface(). is faster than MethodByName or pre-selected methods.
|
||||
b := ctrl.Interface().(BaseController)
|
||||
// init the request.
|
||||
b.BeginRequest(ctx)
|
||||
|
||||
// if begin request stopped the execution.
|
||||
if ctx.IsStopped() {
|
||||
return
|
||||
}
|
||||
|
||||
defer b.EndRequest(ctx)
|
||||
}
|
||||
|
||||
if !hasStructInjector && !hasFuncInjector {
|
||||
DispatchFuncResult(ctx, ctrl.Method(m.Index).Call(emptyIn))
|
||||
} else {
|
||||
ctxValue := reflect.ValueOf(ctx)
|
||||
if hasStructInjector {
|
||||
elem := ctrl.Elem()
|
||||
c.injector.InjectElem(elem, ctxValue)
|
||||
if ctx.IsStopped() {
|
||||
return
|
||||
}
|
||||
|
||||
// we do this in order to reduce in := make...
|
||||
// if not func input binders, we execute the handler with empty input args.
|
||||
if !hasFuncInjector {
|
||||
DispatchFuncResult(ctx, ctrl.Method(m.Index).Call(emptyIn))
|
||||
}
|
||||
}
|
||||
// otherwise, it has one or more valid input binders,
|
||||
// make the input and call the func using those.
|
||||
if hasFuncInjector {
|
||||
in := make([]reflect.Value, n, n)
|
||||
in[0] = ctrl
|
||||
funcInjector.Inject(&in, ctxValue)
|
||||
if ctx.IsStopped() {
|
||||
return
|
||||
}
|
||||
|
||||
DispatchFuncResult(ctx, m.Func.Call(in))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// register the handler now.
|
||||
route := c.Router.Handle(method, path, append(middleware, handler)...)
|
||||
if route != nil {
|
||||
// change the main handler's name in order to respect the controller's and give
|
||||
// a proper debug message.
|
||||
route.MainHandlerName = fmt.Sprintf("%s.%s", c.FullName, funcName)
|
||||
}
|
||||
|
||||
return route
|
||||
}
|
||||
85
mvc/controller_handle_test.go
Normal file
85
mvc/controller_handle_test.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package mvc_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/kataras/iris"
|
||||
"github.com/kataras/iris/context"
|
||||
"github.com/kataras/iris/httptest"
|
||||
|
||||
. "github.com/kataras/iris/mvc"
|
||||
)
|
||||
|
||||
type testControllerHandle struct {
|
||||
Ctx context.Context
|
||||
Service TestService
|
||||
|
||||
reqField string
|
||||
}
|
||||
|
||||
func (c *testControllerHandle) BeforeActivate(ca *ControllerActivator) { // BeforeActivate(t *mvc.TController) {
|
||||
ca.Handle("GET", "/histatic", "HiStatic")
|
||||
ca.Handle("GET", "/hiservice", "HiService")
|
||||
ca.Handle("GET", "/hiparam/{ps:string}", "HiParamBy")
|
||||
ca.Handle("GET", "/hiparamempyinput/{ps:string}", "HiParamEmptyInputBy")
|
||||
}
|
||||
|
||||
func (c *testControllerHandle) BeginRequest(ctx iris.Context) {
|
||||
c.reqField = ctx.URLParam("reqfield")
|
||||
}
|
||||
|
||||
func (c *testControllerHandle) EndRequest(ctx iris.Context) {}
|
||||
|
||||
func (c *testControllerHandle) Get() string {
|
||||
return "index"
|
||||
}
|
||||
|
||||
func (c *testControllerHandle) HiStatic() string {
|
||||
return c.reqField
|
||||
}
|
||||
|
||||
func (c *testControllerHandle) HiService() string {
|
||||
return c.Service.Say("hi")
|
||||
}
|
||||
|
||||
func (c *testControllerHandle) HiParamBy(v string) string {
|
||||
return v
|
||||
}
|
||||
|
||||
func (c *testControllerHandle) HiParamEmptyInputBy() string {
|
||||
return "empty in but served with ctx.Params.Get('ps')=" + c.Ctx.Params().Get("ps")
|
||||
}
|
||||
|
||||
func TestControllerHandle(t *testing.T) {
|
||||
app := iris.New()
|
||||
|
||||
m := NewEngine()
|
||||
m.Dependencies.Add(&TestServiceImpl{prefix: "service:"})
|
||||
m.Controller(app, new(testControllerHandle))
|
||||
|
||||
e := httptest.New(t, app)
|
||||
|
||||
// test the index, is not part of the current package's implementation but do it.
|
||||
e.GET("/").Expect().Status(httptest.StatusOK).Body().Equal("index")
|
||||
|
||||
// the important things now.
|
||||
|
||||
// this test ensures that the BeginRequest of the controller will be
|
||||
// called correctly and also the controller is binded to the first input argument
|
||||
// (which is the function's receiver, if any, in this case the *testController in go).
|
||||
expectedReqField := "this is a request field filled by this url param"
|
||||
e.GET("/histatic").WithQuery("reqfield", expectedReqField).Expect().Status(httptest.StatusOK).
|
||||
Body().Equal(expectedReqField)
|
||||
// this test makes sure that the binded values of the controller is handled correctly
|
||||
// and can be used in a user-defined, dynamic "mvc handler".
|
||||
e.GET("/hiservice").Expect().Status(httptest.StatusOK).
|
||||
Body().Equal("service: hi")
|
||||
|
||||
// this worked with a temporary variadic on the resolvemethodfunc which is not
|
||||
// correct design, I should split the path and params with the rest of implementation
|
||||
// in order a simple template.Src can be given.
|
||||
e.GET("/hiparam/value").Expect().Status(httptest.StatusOK).
|
||||
Body().Equal("value")
|
||||
e.GET("/hiparamempyinput/value").Expect().Status(httptest.StatusOK).
|
||||
Body().Equal("empty in but served with ctx.Params.Get('ps')=value")
|
||||
}
|
||||
254
mvc/controller_method_parser.go
Normal file
254
mvc/controller_method_parser.go
Normal file
@@ -0,0 +1,254 @@
|
||||
package mvc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/kataras/iris/core/router"
|
||||
"github.com/kataras/iris/core/router/macro/interpreter/ast"
|
||||
)
|
||||
|
||||
const (
|
||||
tokenBy = "By"
|
||||
tokenWildcard = "Wildcard" // "ByWildcard".
|
||||
)
|
||||
|
||||
// word lexer, not characters.
|
||||
type methodLexer struct {
|
||||
words []string
|
||||
cur int
|
||||
}
|
||||
|
||||
func newMethodLexer(s string) *methodLexer {
|
||||
l := new(methodLexer)
|
||||
l.reset(s)
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *methodLexer) reset(s string) {
|
||||
l.cur = -1
|
||||
var words []string
|
||||
if s != "" {
|
||||
end := len(s)
|
||||
start := -1
|
||||
|
||||
for i, n := 0, end; i < n; i++ {
|
||||
c := rune(s[i])
|
||||
if unicode.IsUpper(c) {
|
||||
// it doesn't count the last uppercase
|
||||
if start != -1 {
|
||||
end = i
|
||||
words = append(words, s[start:end])
|
||||
}
|
||||
start = i
|
||||
continue
|
||||
}
|
||||
end = i + 1
|
||||
}
|
||||
|
||||
if end > 0 && len(s) >= end {
|
||||
words = append(words, s[start:end])
|
||||
}
|
||||
}
|
||||
|
||||
l.words = words
|
||||
}
|
||||
|
||||
func (l *methodLexer) next() (w string) {
|
||||
cur := l.cur + 1
|
||||
|
||||
if w = l.peek(cur); w != "" {
|
||||
l.cur++
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (l *methodLexer) skip() {
|
||||
if cur := l.cur + 1; cur < len(l.words) {
|
||||
l.cur = cur
|
||||
} else {
|
||||
l.cur = len(l.words) - 1
|
||||
}
|
||||
}
|
||||
|
||||
func (l *methodLexer) peek(idx int) string {
|
||||
if idx < len(l.words) {
|
||||
return l.words[idx]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (l *methodLexer) peekNext() (w string) {
|
||||
return l.peek(l.cur + 1)
|
||||
}
|
||||
|
||||
func (l *methodLexer) peekPrev() (w string) {
|
||||
if l.cur > 0 {
|
||||
cur := l.cur - 1
|
||||
w = l.words[cur]
|
||||
}
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
var posWords = map[int]string{
|
||||
0: "",
|
||||
1: "first",
|
||||
2: "second",
|
||||
3: "third",
|
||||
4: "forth",
|
||||
5: "five",
|
||||
6: "sixth",
|
||||
7: "seventh",
|
||||
8: "eighth",
|
||||
9: "ninth",
|
||||
10: "tenth",
|
||||
11: "eleventh",
|
||||
12: "twelfth",
|
||||
13: "thirteenth",
|
||||
14: "fourteenth",
|
||||
15: "fifteenth",
|
||||
16: "sixteenth",
|
||||
17: "seventeenth",
|
||||
18: "eighteenth",
|
||||
19: "nineteenth",
|
||||
20: "twentieth",
|
||||
}
|
||||
|
||||
func genParamKey(argIdx int) string {
|
||||
return "arg" + posWords[argIdx] // argfirst, argsecond...
|
||||
}
|
||||
|
||||
type methodParser struct {
|
||||
lexer *methodLexer
|
||||
fn reflect.Method
|
||||
}
|
||||
|
||||
func parseMethod(fn reflect.Method, skipper func(string) bool) (method, path string, err error) {
|
||||
if skipper(fn.Name) {
|
||||
return "", "", errSkip
|
||||
}
|
||||
|
||||
p := &methodParser{
|
||||
fn: fn,
|
||||
lexer: newMethodLexer(fn.Name),
|
||||
}
|
||||
return p.parse()
|
||||
}
|
||||
|
||||
func methodTitle(httpMethod string) string {
|
||||
httpMethodFuncName := strings.Title(strings.ToLower(httpMethod))
|
||||
return httpMethodFuncName
|
||||
}
|
||||
|
||||
var errSkip = errors.New("skip")
|
||||
|
||||
var allMethods = append(router.AllMethods[0:], []string{"ALL", "ANY"}...)
|
||||
|
||||
func (p *methodParser) parse() (method, path string, err error) {
|
||||
funcArgPos := 0
|
||||
path = "/"
|
||||
// take the first word and check for the method.
|
||||
w := p.lexer.next()
|
||||
|
||||
for _, httpMethod := range allMethods {
|
||||
possibleMethodFuncName := methodTitle(httpMethod)
|
||||
if strings.Index(w, possibleMethodFuncName) == 0 {
|
||||
method = httpMethod
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if method == "" {
|
||||
// this is not a valid method to parse, we just skip it,
|
||||
// it may be used for end-dev's use cases.
|
||||
return "", "", errSkip
|
||||
}
|
||||
|
||||
for {
|
||||
w := p.lexer.next()
|
||||
if w == "" {
|
||||
break
|
||||
}
|
||||
|
||||
if w == tokenBy {
|
||||
funcArgPos++ // starting with 1 because in typ.NumIn() the first is the struct receiver.
|
||||
|
||||
// No need for these:
|
||||
// ByBy will act like /{param:type}/{param:type} as users expected
|
||||
// if func input arguments are there, else act By like normal path /by.
|
||||
//
|
||||
// if p.lexer.peekPrev() == tokenBy || typ.NumIn() == 1 { // ByBy, then act this second By like a path
|
||||
// a.relPath += "/" + strings.ToLower(w)
|
||||
// continue
|
||||
// }
|
||||
|
||||
if path, funcArgPos, err = p.parsePathParam(path, w, funcArgPos); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
// static path.
|
||||
path += "/" + strings.ToLower(w)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (p *methodParser) parsePathParam(path string, w string, funcArgPos int) (string, int, error) {
|
||||
typ := p.fn.Type
|
||||
|
||||
if typ.NumIn() <= funcArgPos {
|
||||
|
||||
// By found but input arguments are not there, so act like /by path without restricts.
|
||||
path += "/" + strings.ToLower(w)
|
||||
return path, funcArgPos, nil
|
||||
}
|
||||
|
||||
var (
|
||||
paramKey = genParamKey(funcArgPos) // argfirst, argsecond...
|
||||
paramType = ast.ParamTypeString // default string
|
||||
)
|
||||
|
||||
// string, int...
|
||||
goType := typ.In(funcArgPos).Name()
|
||||
nextWord := p.lexer.peekNext()
|
||||
|
||||
if nextWord == tokenWildcard {
|
||||
p.lexer.skip() // skip the Wildcard word.
|
||||
paramType = ast.ParamTypePath
|
||||
} else if pType := ast.LookupParamTypeFromStd(goType); pType != ast.ParamTypeUnExpected {
|
||||
// it's not wildcard, so check base on our available macro types.
|
||||
paramType = pType
|
||||
} else {
|
||||
if typ.NumIn() > funcArgPos {
|
||||
// has more input arguments but we are not in the correct
|
||||
// index now, maybe the first argument was an `iris/context.Context`
|
||||
// so retry with the "funcArgPos" incremented.
|
||||
//
|
||||
// the "funcArgPos" will be updated to the caller as well
|
||||
// because we return it as well.
|
||||
return p.parsePathParam(path, w, funcArgPos+1)
|
||||
}
|
||||
return "", 0, errors.New("invalid syntax for " + p.fn.Name)
|
||||
}
|
||||
|
||||
// /{argfirst:path}, /{argfirst:long}...
|
||||
path += fmt.Sprintf("/{%s:%s}", paramKey, paramType.String())
|
||||
|
||||
if nextWord == "" && typ.NumIn() > funcArgPos+1 {
|
||||
// By is the latest word but func is expected
|
||||
// more path parameters values, i.e:
|
||||
// GetBy(name string, age int)
|
||||
// The caller (parse) doesn't need to know
|
||||
// about the incremental funcArgPos because
|
||||
// it will not need it.
|
||||
return p.parsePathParam(path, nextWord, funcArgPos+1)
|
||||
}
|
||||
|
||||
return path, funcArgPos, nil
|
||||
}
|
||||
454
mvc/controller_test.go
Normal file
454
mvc/controller_test.go
Normal file
@@ -0,0 +1,454 @@
|
||||
// black-box testing
|
||||
package mvc_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/kataras/iris"
|
||||
"github.com/kataras/iris/context"
|
||||
"github.com/kataras/iris/core/router"
|
||||
"github.com/kataras/iris/httptest"
|
||||
|
||||
. "github.com/kataras/iris/mvc"
|
||||
)
|
||||
|
||||
type testController struct {
|
||||
Ctx context.Context
|
||||
}
|
||||
|
||||
var writeMethod = func(ctx context.Context) {
|
||||
ctx.Writef(ctx.Method())
|
||||
}
|
||||
|
||||
func (c *testController) Get() {
|
||||
writeMethod(c.Ctx)
|
||||
}
|
||||
func (c *testController) Post() {
|
||||
writeMethod(c.Ctx)
|
||||
}
|
||||
func (c *testController) Put() {
|
||||
writeMethod(c.Ctx)
|
||||
}
|
||||
func (c *testController) Delete() {
|
||||
writeMethod(c.Ctx)
|
||||
}
|
||||
func (c *testController) Connect() {
|
||||
writeMethod(c.Ctx)
|
||||
}
|
||||
func (c *testController) Head() {
|
||||
writeMethod(c.Ctx)
|
||||
}
|
||||
func (c *testController) Patch() {
|
||||
writeMethod(c.Ctx)
|
||||
}
|
||||
func (c *testController) Options() {
|
||||
writeMethod(c.Ctx)
|
||||
}
|
||||
func (c *testController) Trace() {
|
||||
writeMethod(c.Ctx)
|
||||
}
|
||||
|
||||
type (
|
||||
testControllerAll struct{ Ctx context.Context }
|
||||
testControllerAny struct{ Ctx context.Context } // exactly the same as All.
|
||||
)
|
||||
|
||||
func (c *testControllerAll) All() {
|
||||
writeMethod(c.Ctx)
|
||||
}
|
||||
|
||||
func (c *testControllerAny) Any() {
|
||||
writeMethod(c.Ctx)
|
||||
}
|
||||
|
||||
func TestControllerMethodFuncs(t *testing.T) {
|
||||
app := iris.New()
|
||||
|
||||
m := NewEngine()
|
||||
m.Controller(app, new(testController))
|
||||
m.Controller(app.Party("/all"), new(testControllerAll))
|
||||
m.Controller(app.Party("/any"), new(testControllerAny))
|
||||
|
||||
e := httptest.New(t, app)
|
||||
for _, method := range router.AllMethods {
|
||||
|
||||
e.Request(method, "/").Expect().Status(iris.StatusOK).
|
||||
Body().Equal(method)
|
||||
|
||||
e.Request(method, "/all").Expect().Status(iris.StatusOK).
|
||||
Body().Equal(method)
|
||||
|
||||
e.Request(method, "/any").Expect().Status(iris.StatusOK).
|
||||
Body().Equal(method)
|
||||
}
|
||||
}
|
||||
|
||||
type testControllerBeginAndEndRequestFunc struct {
|
||||
Ctx context.Context
|
||||
|
||||
Username string
|
||||
}
|
||||
|
||||
// called before of every method (Get() or Post()).
|
||||
//
|
||||
// useful when more than one methods using the
|
||||
// same request values or context's function calls.
|
||||
func (c *testControllerBeginAndEndRequestFunc) BeginRequest(ctx context.Context) {
|
||||
c.Username = ctx.Params().Get("username")
|
||||
}
|
||||
|
||||
// called after every method (Get() or Post()).
|
||||
func (c *testControllerBeginAndEndRequestFunc) EndRequest(ctx context.Context) {
|
||||
ctx.Writef("done") // append "done" to the response
|
||||
}
|
||||
|
||||
func (c *testControllerBeginAndEndRequestFunc) Get() {
|
||||
c.Ctx.Writef(c.Username)
|
||||
}
|
||||
|
||||
func (c *testControllerBeginAndEndRequestFunc) Post() {
|
||||
c.Ctx.Writef(c.Username)
|
||||
}
|
||||
|
||||
func TestControllerBeginAndEndRequestFunc(t *testing.T) {
|
||||
app := iris.New()
|
||||
NewEngine().Controller(app.Party("/profile/{username}"), new(testControllerBeginAndEndRequestFunc))
|
||||
|
||||
e := httptest.New(t, app)
|
||||
usernames := []string{
|
||||
"kataras",
|
||||
"makis",
|
||||
"efi",
|
||||
"rg",
|
||||
"bill",
|
||||
"whoisyourdaddy",
|
||||
}
|
||||
doneResponse := "done"
|
||||
|
||||
for _, username := range usernames {
|
||||
e.GET("/profile/" + username).Expect().Status(iris.StatusOK).
|
||||
Body().Equal(username + doneResponse)
|
||||
e.POST("/profile/" + username).Expect().Status(iris.StatusOK).
|
||||
Body().Equal(username + doneResponse)
|
||||
}
|
||||
}
|
||||
|
||||
func TestControllerBeginAndEndRequestFuncBindMiddleware(t *testing.T) {
|
||||
app := iris.New()
|
||||
usernames := map[string]bool{
|
||||
"kataras": true,
|
||||
"makis": false,
|
||||
"efi": true,
|
||||
"rg": false,
|
||||
"bill": true,
|
||||
"whoisyourdaddy": false,
|
||||
}
|
||||
middlewareCheck := func(ctx context.Context) {
|
||||
for username, allow := range usernames {
|
||||
if ctx.Params().Get("username") == username && allow {
|
||||
ctx.Next()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.StatusCode(iris.StatusForbidden)
|
||||
ctx.Writef("forbidden")
|
||||
}
|
||||
|
||||
NewEngine().Controller(app.Party("/profile/{username}", middlewareCheck),
|
||||
new(testControllerBeginAndEndRequestFunc))
|
||||
|
||||
e := httptest.New(t, app)
|
||||
|
||||
doneResponse := "done"
|
||||
|
||||
for username, allow := range usernames {
|
||||
getEx := e.GET("/profile/" + username).Expect()
|
||||
if allow {
|
||||
getEx.Status(iris.StatusOK).
|
||||
Body().Equal(username + doneResponse)
|
||||
} else {
|
||||
getEx.Status(iris.StatusForbidden).Body().Equal("forbidden")
|
||||
}
|
||||
|
||||
postEx := e.POST("/profile/" + username).Expect()
|
||||
if allow {
|
||||
postEx.Status(iris.StatusOK).
|
||||
Body().Equal(username + doneResponse)
|
||||
} else {
|
||||
postEx.Status(iris.StatusForbidden).Body().Equal("forbidden")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
Username string
|
||||
}
|
||||
|
||||
type testControllerEndRequestAwareness struct {
|
||||
Ctx context.Context
|
||||
}
|
||||
|
||||
func (c *testControllerEndRequestAwareness) Get() {
|
||||
username := c.Ctx.Params().Get("username")
|
||||
c.Ctx.Values().Set(c.Ctx.Application().ConfigurationReadOnly().GetViewDataContextKey(),
|
||||
map[string]interface{}{
|
||||
"TestModel": Model{Username: username},
|
||||
"myModel": Model{Username: username + "2"},
|
||||
})
|
||||
}
|
||||
|
||||
func writeModels(ctx context.Context, names ...string) {
|
||||
if expected, got := len(names), len(ctx.GetViewData()); expected != got {
|
||||
ctx.Writef("expected view data length: %d but got: %d for names: %s", expected, got, names)
|
||||
return
|
||||
}
|
||||
|
||||
for _, name := range names {
|
||||
|
||||
m, ok := ctx.GetViewData()[name]
|
||||
if !ok {
|
||||
ctx.Writef("fail load and set the %s", name)
|
||||
return
|
||||
}
|
||||
|
||||
model, ok := m.(Model)
|
||||
if !ok {
|
||||
ctx.Writef("fail to override the %s' name by the tag", name)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Writef(model.Username)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *testControllerEndRequestAwareness) BeginRequest(ctx context.Context) {}
|
||||
func (c *testControllerEndRequestAwareness) EndRequest(ctx context.Context) {
|
||||
writeModels(ctx, "TestModel", "myModel")
|
||||
}
|
||||
|
||||
func TestControllerEndRequestAwareness(t *testing.T) {
|
||||
app := iris.New()
|
||||
NewEngine().Controller(app.Party("/era/{username}"), new(testControllerEndRequestAwareness))
|
||||
|
||||
e := httptest.New(t, app)
|
||||
usernames := []string{
|
||||
"kataras",
|
||||
"makis",
|
||||
}
|
||||
|
||||
for _, username := range usernames {
|
||||
e.GET("/era/" + username).Expect().Status(iris.StatusOK).
|
||||
Body().Equal(username + username + "2")
|
||||
}
|
||||
}
|
||||
|
||||
type testBindType struct {
|
||||
title string
|
||||
}
|
||||
|
||||
type testControllerBindStruct struct {
|
||||
Ctx context.Context
|
||||
|
||||
// should start with upper letter of course
|
||||
TitlePointer *testBindType // should have the value of the "myTitlePtr" on test
|
||||
TitleValue testBindType // should have the value of the "myTitleV" on test
|
||||
Other string // just another type to check the field collection, should be empty
|
||||
}
|
||||
|
||||
func (t *testControllerBindStruct) Get() {
|
||||
t.Ctx.Writef(t.TitlePointer.title + t.TitleValue.title + t.Other)
|
||||
}
|
||||
|
||||
// test if context can be binded to the controller's function
|
||||
// without need to declare it to a struct if not needed.
|
||||
func (t *testControllerBindStruct) GetCtx(ctx iris.Context) {
|
||||
ctx.StatusCode(iris.StatusContinue)
|
||||
}
|
||||
|
||||
type testControllerBindDeep struct {
|
||||
testControllerBindStruct
|
||||
}
|
||||
|
||||
func (t *testControllerBindDeep) Get() {
|
||||
// t.testControllerBindStruct.Get()
|
||||
t.Ctx.Writef(t.TitlePointer.title + t.TitleValue.title + t.Other)
|
||||
}
|
||||
|
||||
func TestControllerBind(t *testing.T) {
|
||||
app := iris.New()
|
||||
// app.Logger().SetLevel("debug")
|
||||
|
||||
t1, t2 := "my pointer title", "val title"
|
||||
// test bind pointer to pointer of the correct type
|
||||
myTitlePtr := &testBindType{title: t1}
|
||||
// test bind value to value of the correct type
|
||||
myTitleV := testBindType{title: t2}
|
||||
m := NewEngine()
|
||||
m.Dependencies.Add(myTitlePtr, myTitleV)
|
||||
// or just app
|
||||
m.Controller(app.Party("/"), new(testControllerBindStruct))
|
||||
m.Controller(app.Party("/deep"), new(testControllerBindDeep))
|
||||
|
||||
e := httptest.New(t, app)
|
||||
expected := t1 + t2
|
||||
e.GET("/").Expect().Status(iris.StatusOK).
|
||||
Body().Equal(expected)
|
||||
e.GET("/ctx").Expect().Status(iris.StatusContinue)
|
||||
|
||||
e.GET("/deep").Expect().Status(iris.StatusOK).
|
||||
Body().Equal(expected)
|
||||
}
|
||||
|
||||
type testCtrl0 struct {
|
||||
testCtrl00
|
||||
}
|
||||
|
||||
func (c *testCtrl0) Get() string {
|
||||
return c.Ctx.Params().Get("username")
|
||||
}
|
||||
|
||||
func (c *testCtrl0) EndRequest(ctx context.Context) {
|
||||
if c.TitlePointer == nil {
|
||||
ctx.Writef("\nTitlePointer is nil!\n")
|
||||
} else {
|
||||
ctx.Writef(c.TitlePointer.title)
|
||||
}
|
||||
|
||||
//should be the same as `.testCtrl000.testCtrl0000.EndRequest(ctx)`
|
||||
c.testCtrl00.EndRequest(ctx)
|
||||
}
|
||||
|
||||
type testCtrl00 struct {
|
||||
Ctx context.Context
|
||||
|
||||
testCtrl000
|
||||
}
|
||||
|
||||
type testCtrl000 struct {
|
||||
testCtrl0000
|
||||
|
||||
TitlePointer *testBindType
|
||||
}
|
||||
|
||||
type testCtrl0000 struct {
|
||||
}
|
||||
|
||||
func (c *testCtrl0000) BeginRequest(ctx context.Context) {}
|
||||
func (c *testCtrl0000) EndRequest(ctx context.Context) {
|
||||
ctx.Writef("finish")
|
||||
}
|
||||
|
||||
func TestControllerInsideControllerRecursively(t *testing.T) {
|
||||
var (
|
||||
username = "gerasimos"
|
||||
title = "mytitle"
|
||||
expected = username + title + "finish"
|
||||
)
|
||||
|
||||
app := iris.New()
|
||||
m := NewEngine()
|
||||
m.Dependencies.Add(&testBindType{title: title})
|
||||
m.Controller(app.Party("/user/{username}"), new(testCtrl0))
|
||||
|
||||
e := httptest.New(t, app)
|
||||
e.GET("/user/" + username).Expect().
|
||||
Status(iris.StatusOK).Body().Equal(expected)
|
||||
}
|
||||
|
||||
type testControllerRelPathFromFunc struct{}
|
||||
|
||||
func (c *testControllerRelPathFromFunc) BeginRequest(ctx context.Context) {}
|
||||
func (c *testControllerRelPathFromFunc) EndRequest(ctx context.Context) {
|
||||
ctx.Writef("%s:%s", ctx.Method(), ctx.Path())
|
||||
}
|
||||
|
||||
func (c *testControllerRelPathFromFunc) Get() {}
|
||||
func (c *testControllerRelPathFromFunc) GetBy(int64) {}
|
||||
func (c *testControllerRelPathFromFunc) GetAnythingByWildcard(string) {}
|
||||
|
||||
func (c *testControllerRelPathFromFunc) GetLogin() {}
|
||||
func (c *testControllerRelPathFromFunc) PostLogin() {}
|
||||
|
||||
func (c *testControllerRelPathFromFunc) GetAdminLogin() {}
|
||||
|
||||
func (c *testControllerRelPathFromFunc) PutSomethingIntoThis() {}
|
||||
|
||||
func (c *testControllerRelPathFromFunc) GetSomethingBy(bool) {}
|
||||
func (c *testControllerRelPathFromFunc) GetSomethingByBy(string, int) {}
|
||||
func (c *testControllerRelPathFromFunc) GetSomethingNewBy(string, int) {} // two input arguments, one By which is the latest word.
|
||||
func (c *testControllerRelPathFromFunc) GetSomethingByElseThisBy(bool, int) {} // two input arguments
|
||||
|
||||
func TestControllerRelPathFromFunc(t *testing.T) {
|
||||
app := iris.New()
|
||||
NewEngine().Controller(app, new(testControllerRelPathFromFunc))
|
||||
|
||||
e := httptest.New(t, app)
|
||||
e.GET("/").Expect().Status(iris.StatusOK).
|
||||
Body().Equal("GET:/")
|
||||
|
||||
e.GET("/42").Expect().Status(iris.StatusOK).
|
||||
Body().Equal("GET:/42")
|
||||
e.GET("/something/true").Expect().Status(iris.StatusOK).
|
||||
Body().Equal("GET:/something/true")
|
||||
e.GET("/something/false").Expect().Status(iris.StatusOK).
|
||||
Body().Equal("GET:/something/false")
|
||||
e.GET("/something/truee").Expect().Status(iris.StatusNotFound)
|
||||
e.GET("/something/falsee").Expect().Status(iris.StatusNotFound)
|
||||
e.GET("/something/kataras/42").Expect().Status(iris.StatusOK).
|
||||
Body().Equal("GET:/something/kataras/42")
|
||||
e.GET("/something/new/kataras/42").Expect().Status(iris.StatusOK).
|
||||
Body().Equal("GET:/something/new/kataras/42")
|
||||
e.GET("/something/true/else/this/42").Expect().Status(iris.StatusOK).
|
||||
Body().Equal("GET:/something/true/else/this/42")
|
||||
|
||||
e.GET("/login").Expect().Status(iris.StatusOK).
|
||||
Body().Equal("GET:/login")
|
||||
e.POST("/login").Expect().Status(iris.StatusOK).
|
||||
Body().Equal("POST:/login")
|
||||
e.GET("/admin/login").Expect().Status(iris.StatusOK).
|
||||
Body().Equal("GET:/admin/login")
|
||||
e.PUT("/something/into/this").Expect().Status(iris.StatusOK).
|
||||
Body().Equal("PUT:/something/into/this")
|
||||
e.GET("/42").Expect().Status(iris.StatusOK).
|
||||
Body().Equal("GET:/42")
|
||||
e.GET("/anything/here").Expect().Status(iris.StatusOK).
|
||||
Body().Equal("GET:/anything/here")
|
||||
|
||||
}
|
||||
|
||||
type testControllerActivateListener struct {
|
||||
TitlePointer *testBindType
|
||||
}
|
||||
|
||||
func (c *testControllerActivateListener) BeforeActivate(ca *ControllerActivator) {
|
||||
ca.Dependencies.AddOnce(&testBindType{title: "default title"})
|
||||
}
|
||||
|
||||
func (c *testControllerActivateListener) Get() string {
|
||||
return c.TitlePointer.title
|
||||
}
|
||||
|
||||
func TestControllerActivateListener(t *testing.T) {
|
||||
app := iris.New()
|
||||
NewEngine().Controller(app, new(testControllerActivateListener))
|
||||
m := NewEngine()
|
||||
m.Dependencies.Add(&testBindType{ // will bind to all controllers under this .New() MVC Engine.
|
||||
title: "my title",
|
||||
})
|
||||
m.Controller(app.Party("/manual"), new(testControllerActivateListener))
|
||||
// or
|
||||
NewEngine().Controller(app.Party("/manual2"), &testControllerActivateListener{
|
||||
TitlePointer: &testBindType{
|
||||
title: "my title",
|
||||
},
|
||||
})
|
||||
|
||||
e := httptest.New(t, app)
|
||||
e.GET("/").Expect().Status(iris.StatusOK).
|
||||
Body().Equal("default title")
|
||||
e.GET("/manual").Expect().Status(iris.StatusOK).
|
||||
Body().Equal("my title")
|
||||
e.GET("/manual2").Expect().Status(iris.StatusOK).
|
||||
Body().Equal("my title")
|
||||
}
|
||||
92
mvc/di/di.go
Normal file
92
mvc/di/di.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package di
|
||||
|
||||
import "reflect"
|
||||
|
||||
type (
|
||||
// Hijacker is a type which is used to catch fields or function's input argument
|
||||
// to bind a custom object based on their type.
|
||||
Hijacker func(reflect.Type) (*BindObject, bool)
|
||||
// TypeChecker checks if a specific field's or function input argument's
|
||||
// is valid to be binded.
|
||||
TypeChecker func(reflect.Type) bool
|
||||
)
|
||||
|
||||
// D is the Dependency Injection container,
|
||||
// it contains the Values that can be changed before the injectors.
|
||||
// `Struct` and the `Func` methods returns an injector for specific
|
||||
// struct instance-value or function.
|
||||
type D struct {
|
||||
Values
|
||||
|
||||
hijacker Hijacker
|
||||
goodFunc TypeChecker
|
||||
}
|
||||
|
||||
// New creates and returns a new Dependency Injection container.
|
||||
// See `Values` field and `Func` and `Struct` methods for more.
|
||||
func New() *D {
|
||||
return &D{}
|
||||
}
|
||||
|
||||
// Hijack sets a hijacker function, read the `Hijacker` type for more explaination.
|
||||
func (d *D) Hijack(fn Hijacker) *D {
|
||||
d.hijacker = fn
|
||||
return d
|
||||
}
|
||||
|
||||
// GoodFunc sets a type checker for a valid function that can be binded,
|
||||
// read the `TypeChecker` type for more explaination.
|
||||
func (d *D) GoodFunc(fn TypeChecker) *D {
|
||||
d.goodFunc = fn
|
||||
return d
|
||||
}
|
||||
|
||||
// Clone returns a new Dependency Injection container, it adopts the
|
||||
// parent's (current "D") hijacker, good func type checker and all dependencies values.
|
||||
func (d *D) Clone() *D {
|
||||
clone := New()
|
||||
clone.hijacker = d.hijacker
|
||||
clone.goodFunc = d.goodFunc
|
||||
|
||||
// copy the current dynamic bindings (func binders)
|
||||
// and static struct bindings (services) to this new child.
|
||||
if n := len(d.Values); n > 0 {
|
||||
values := make(Values, n, n)
|
||||
copy(values, d.Values)
|
||||
clone.Values = values
|
||||
}
|
||||
|
||||
return clone
|
||||
}
|
||||
|
||||
// Struct is being used to return a new injector based on
|
||||
// a struct value instance, if it contains fields that the types of those
|
||||
// are matching with one or more of the `Values` then they are binded
|
||||
// with the injector's `Inject` and `InjectElem` methods.
|
||||
func (d *D) Struct(s interface{}) *StructInjector {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
v := ValueOf(s)
|
||||
|
||||
return MakeStructInjector(
|
||||
v,
|
||||
d.hijacker,
|
||||
d.goodFunc,
|
||||
d.Values...,
|
||||
)
|
||||
}
|
||||
|
||||
// Func is being used to return a new injector based on
|
||||
// a function, if it contains input arguments that the types of those
|
||||
// are matching with one or more of the `Values` then they are binded
|
||||
// to the function's input argument when called
|
||||
// with the injector's `Fill` method.
|
||||
func (d *D) Func(fn interface{}) *FuncInjector {
|
||||
return MakeFuncInjector(
|
||||
ValueOf(fn),
|
||||
d.hijacker,
|
||||
d.goodFunc,
|
||||
d.Values...,
|
||||
)
|
||||
}
|
||||
107
mvc/di/func.go
Normal file
107
mvc/di/func.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package di
|
||||
|
||||
import "reflect"
|
||||
|
||||
type (
|
||||
targetFuncInput struct {
|
||||
Object *BindObject
|
||||
InputIndex int
|
||||
}
|
||||
|
||||
FuncInjector struct {
|
||||
// the original function, is being used
|
||||
// only the .Call, which is refering to the same function, always.
|
||||
fn reflect.Value
|
||||
|
||||
inputs []*targetFuncInput
|
||||
// Length is the number of the valid, final binded input arguments.
|
||||
Length int
|
||||
// Valid is True when `Length` is > 0, it's statically set-ed for
|
||||
// performance reasons.
|
||||
Valid bool //
|
||||
}
|
||||
)
|
||||
|
||||
func MakeFuncInjector(fn reflect.Value, hijack Hijacker, goodFunc TypeChecker, values ...reflect.Value) *FuncInjector {
|
||||
typ := IndirectType(fn.Type())
|
||||
s := &FuncInjector{
|
||||
fn: fn,
|
||||
}
|
||||
|
||||
if !IsFunc(typ) {
|
||||
return s
|
||||
}
|
||||
|
||||
n := typ.NumIn()
|
||||
|
||||
// function input can have many values of the same types,
|
||||
// so keep track of them in order to not set a func input to a next bind value,
|
||||
// i.e (string, string) with two different binder funcs because of the different param's name.
|
||||
consumedValues := make(map[int]bool, n)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
inTyp := typ.In(i)
|
||||
|
||||
if hijack != nil {
|
||||
if b, ok := hijack(inTyp); ok && b != nil {
|
||||
s.inputs = append(s.inputs, &targetFuncInput{
|
||||
InputIndex: i,
|
||||
Object: b,
|
||||
})
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
for valIdx, val := range values {
|
||||
if _, shouldSkip := consumedValues[valIdx]; shouldSkip {
|
||||
continue
|
||||
}
|
||||
inTyp := typ.In(i)
|
||||
|
||||
// the binded values to the func's inputs.
|
||||
b, err := MakeBindObject(val, goodFunc)
|
||||
|
||||
if err != nil {
|
||||
return s // if error stop here.
|
||||
}
|
||||
|
||||
if b.IsAssignable(inTyp) {
|
||||
// println(inTyp.String() + " is assignable to " + val.Type().String())
|
||||
// fmt.Printf("binded input index: %d for type: %s and value: %v with pointer: %v\n",
|
||||
// i, b.Type.String(), val.String(), val.Pointer())
|
||||
s.inputs = append(s.inputs, &targetFuncInput{
|
||||
InputIndex: i,
|
||||
Object: &b,
|
||||
})
|
||||
|
||||
consumedValues[valIdx] = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// s.Length = n
|
||||
s.Length = len(s.inputs)
|
||||
s.Valid = s.Length > 0
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *FuncInjector) Inject(in *[]reflect.Value, ctx ...reflect.Value) {
|
||||
args := *in
|
||||
for _, input := range s.inputs {
|
||||
input.Object.Assign(ctx, func(v reflect.Value) {
|
||||
// fmt.Printf("assign input index: %d for value: %v\n",
|
||||
// input.InputIndex, v.String())
|
||||
args[input.InputIndex] = v
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
*in = args
|
||||
}
|
||||
|
||||
func (s *FuncInjector) Call(ctx ...reflect.Value) []reflect.Value {
|
||||
in := make([]reflect.Value, s.Length, s.Length)
|
||||
s.Inject(&in, ctx...)
|
||||
return s.fn.Call(in)
|
||||
}
|
||||
97
mvc/di/object.go
Normal file
97
mvc/di/object.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package di
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type BindType uint32
|
||||
|
||||
const (
|
||||
Static BindType = iota // simple assignable value, a static value.
|
||||
Dynamic // dynamic value, depends on some input arguments from the caller.
|
||||
)
|
||||
|
||||
type BindObject struct {
|
||||
Type reflect.Type // the Type of 'Value' or the type of the returned 'ReturnValue' .
|
||||
Value reflect.Value
|
||||
|
||||
BindType BindType
|
||||
ReturnValue func([]reflect.Value) reflect.Value
|
||||
}
|
||||
|
||||
func MakeBindObject(v reflect.Value, goodFunc TypeChecker) (b BindObject, err error) {
|
||||
if IsFunc(v) {
|
||||
b.BindType = Dynamic
|
||||
b.ReturnValue, b.Type, err = MakeReturnValue(v, goodFunc)
|
||||
} else {
|
||||
b.BindType = Static
|
||||
b.Type = v.Type()
|
||||
b.Value = v
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var errBad = errors.New("bad")
|
||||
|
||||
// MakeReturnValue takes any function
|
||||
// that accept custom values and returns something,
|
||||
// it returns a binder function, which accepts a slice of reflect.Value
|
||||
// and returns a single one reflect.Value for that.
|
||||
// It's being used to resolve the input parameters on a "x" consumer faster.
|
||||
//
|
||||
// The "fn" can have the following form:
|
||||
// `func(myService) MyViewModel`.
|
||||
//
|
||||
// The return type of the "fn" should be a value instance, not a pointer, for your own protection.
|
||||
// The binder function should return only one value.
|
||||
func MakeReturnValue(fn reflect.Value, goodFunc TypeChecker) (func([]reflect.Value) reflect.Value, reflect.Type, error) {
|
||||
typ := IndirectType(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
|
||||
}
|
||||
|
||||
if goodFunc != nil {
|
||||
if !goodFunc(typ) {
|
||||
return nil, typ, errBad
|
||||
}
|
||||
}
|
||||
|
||||
outTyp := typ.Out(0)
|
||||
zeroOutVal := reflect.New(outTyp).Elem()
|
||||
|
||||
bf := func(ctxValue []reflect.Value) reflect.Value {
|
||||
results := fn.Call(ctxValue)
|
||||
if len(results) == 0 {
|
||||
return zeroOutVal
|
||||
}
|
||||
|
||||
v := results[0]
|
||||
if !v.IsValid() {
|
||||
return zeroOutVal
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
return bf, outTyp, nil
|
||||
}
|
||||
|
||||
func (b *BindObject) IsAssignable(to reflect.Type) bool {
|
||||
return equalTypes(b.Type, to)
|
||||
}
|
||||
|
||||
func (b *BindObject) Assign(ctx []reflect.Value, toSetter func(reflect.Value)) {
|
||||
if b.BindType == Dynamic {
|
||||
toSetter(b.ReturnValue(ctx))
|
||||
return
|
||||
}
|
||||
toSetter(b.Value)
|
||||
}
|
||||
180
mvc/di/reflect.go
Normal file
180
mvc/di/reflect.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package di
|
||||
|
||||
import "reflect"
|
||||
|
||||
var emptyIn = []reflect.Value{}
|
||||
|
||||
// IsZero returns true if a value is nil, remember boolean's false is zero.
|
||||
// Remember; fields to be checked should be exported otherwise it returns false.
|
||||
func IsZero(v reflect.Value) bool {
|
||||
switch v.Kind() {
|
||||
case reflect.Struct:
|
||||
zero := true
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
zero = zero && IsZero(v.Field(i))
|
||||
}
|
||||
|
||||
if typ := v.Type(); typ != nil && v.IsValid() {
|
||||
f, ok := typ.MethodByName("IsZero")
|
||||
// if not found
|
||||
// if has input arguments (1 is for the value receiver, so > 1 for the actual input args)
|
||||
// if output argument is not boolean
|
||||
// then skip this IsZero user-defined function.
|
||||
if !ok || f.Type.NumIn() > 1 || f.Type.NumOut() != 1 && f.Type.Out(0).Kind() != reflect.Bool {
|
||||
return zero
|
||||
}
|
||||
|
||||
method := v.Method(f.Index)
|
||||
// no needed check but:
|
||||
if method.IsValid() && !method.IsNil() {
|
||||
// it shouldn't panic here.
|
||||
zero = method.Call(emptyIn)[0].Interface().(bool)
|
||||
}
|
||||
}
|
||||
|
||||
return zero
|
||||
case reflect.Func, reflect.Map, reflect.Slice:
|
||||
return v.IsNil()
|
||||
case reflect.Array:
|
||||
zero := true
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
zero = zero && IsZero(v.Index(i))
|
||||
}
|
||||
return zero
|
||||
}
|
||||
// if not any special type then use the reflect's .Zero
|
||||
// usually for fields, but remember if it's boolean and it's false
|
||||
// then it's zero, even if set-ed.
|
||||
|
||||
if !v.CanInterface() {
|
||||
// if can't interface, i.e return value from unexported field or method then return false
|
||||
return false
|
||||
}
|
||||
zero := reflect.Zero(v.Type())
|
||||
return v.Interface() == zero.Interface()
|
||||
}
|
||||
|
||||
func IndirectValue(v reflect.Value) reflect.Value {
|
||||
return reflect.Indirect(v)
|
||||
}
|
||||
|
||||
func ValueOf(o interface{}) reflect.Value {
|
||||
if v, ok := o.(reflect.Value); ok {
|
||||
return v
|
||||
}
|
||||
|
||||
return reflect.ValueOf(o)
|
||||
}
|
||||
|
||||
func IndirectType(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(kindable interface {
|
||||
Kind() reflect.Kind
|
||||
}) bool {
|
||||
return kindable.Kind() == reflect.Func
|
||||
}
|
||||
|
||||
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's fields 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 bind object (as service which means as static value)
|
||||
// and it's filled from the lookupFields' caller.
|
||||
AnyValue reflect.Value
|
||||
}
|
||||
|
||||
func lookupFields(elemTyp reflect.Type, parentIndex []int) (fields []field) {
|
||||
if elemTyp.Kind() != reflect.Struct {
|
||||
return
|
||||
}
|
||||
|
||||
for i, n := 0, elemTyp.NumField(); i < n; i++ {
|
||||
f := elemTyp.Field(i)
|
||||
|
||||
if IndirectType(f.Type).Kind() == reflect.Struct &&
|
||||
!structFieldIgnored(f) {
|
||||
fields = append(fields, lookupFields(f.Type, append(parentIndex, i))...)
|
||||
continue
|
||||
}
|
||||
|
||||
// skip unexported fields here,
|
||||
// after the check for embedded structs, these can be binded if their
|
||||
// fields are exported.
|
||||
if f.PkgPath != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
index := []int{i}
|
||||
if len(parentIndex) > 0 {
|
||||
index = append(parentIndex, i)
|
||||
}
|
||||
|
||||
field := field{
|
||||
Type: f.Type,
|
||||
Name: f.Name,
|
||||
Index: index,
|
||||
}
|
||||
|
||||
fields = append(fields, field)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// LookupNonZeroFieldsValues lookup for filled fields based on the "v" struct value instance.
|
||||
// It returns a slice of reflect.Value (same type as `Values`) that can be binded,
|
||||
// like the end-developer's custom values.
|
||||
func LookupNonZeroFieldsValues(v reflect.Value) (bindValues []reflect.Value) {
|
||||
elem := IndirectValue(v)
|
||||
fields := lookupFields(IndirectType(v.Type()), nil)
|
||||
for _, f := range fields {
|
||||
|
||||
if fieldVal := elem.FieldByIndex(f.Index); f.Type.Kind() == reflect.Ptr && !IsZero(fieldVal) {
|
||||
bindValues = append(bindValues, fieldVal)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
84
mvc/di/struct.go
Normal file
84
mvc/di/struct.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package di
|
||||
|
||||
import "reflect"
|
||||
|
||||
type (
|
||||
targetStructField struct {
|
||||
Object *BindObject
|
||||
FieldIndex []int
|
||||
}
|
||||
|
||||
StructInjector struct {
|
||||
elemType reflect.Type
|
||||
//
|
||||
fields []*targetStructField
|
||||
Valid bool // is True when contains fields and it's a valid target struct.
|
||||
}
|
||||
)
|
||||
|
||||
func MakeStructInjector(v reflect.Value, hijack Hijacker, goodFunc TypeChecker, values ...reflect.Value) *StructInjector {
|
||||
s := &StructInjector{
|
||||
elemType: IndirectType(v.Type()),
|
||||
}
|
||||
|
||||
fields := lookupFields(s.elemType, nil)
|
||||
for _, f := range fields {
|
||||
|
||||
if hijack != nil {
|
||||
if b, ok := hijack(f.Type); ok && b != nil {
|
||||
s.fields = append(s.fields, &targetStructField{
|
||||
FieldIndex: f.Index,
|
||||
Object: b,
|
||||
})
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
for _, val := range values {
|
||||
// the binded values to the struct's fields.
|
||||
b, err := MakeBindObject(val, goodFunc)
|
||||
|
||||
if err != nil {
|
||||
return s // if error stop here.
|
||||
}
|
||||
|
||||
if b.IsAssignable(f.Type) {
|
||||
// fmt.Printf("bind the object to the field: %s at index: %#v and type: %s\n", f.Name, f.Index, f.Type.String())
|
||||
s.fields = append(s.fields, &targetStructField{
|
||||
FieldIndex: f.Index,
|
||||
Object: &b,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
s.Valid = len(s.fields) > 0
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StructInjector) Inject(dest interface{}, ctx ...reflect.Value) {
|
||||
if dest == nil {
|
||||
return
|
||||
}
|
||||
|
||||
v := IndirectValue(ValueOf(dest))
|
||||
s.InjectElem(v, ctx...)
|
||||
}
|
||||
|
||||
func (s *StructInjector) InjectElem(destElem reflect.Value, ctx ...reflect.Value) {
|
||||
for _, f := range s.fields {
|
||||
f.Object.Assign(ctx, func(v reflect.Value) {
|
||||
// fmt.Printf("%s for %s at index: %d\n", destElem.Type().String(), f.Object.Type.String(), f.FieldIndex)
|
||||
destElem.FieldByIndex(f.FieldIndex).Set(v)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StructInjector) New(ctx ...reflect.Value) reflect.Value {
|
||||
dest := reflect.New(s.elemType)
|
||||
s.InjectElem(dest, ctx...)
|
||||
return dest
|
||||
}
|
||||
98
mvc/di/values.go
Normal file
98
mvc/di/values.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package di
|
||||
|
||||
import "reflect"
|
||||
|
||||
type Values []reflect.Value
|
||||
|
||||
func NewValues() Values {
|
||||
return Values{}
|
||||
}
|
||||
|
||||
// Add binds values to this controller, if you want to share
|
||||
// binding values between controllers use the Engine's `Bind` function instead.
|
||||
func (bv *Values) Add(values ...interface{}) {
|
||||
for _, val := range values {
|
||||
bv.AddValue(reflect.ValueOf(val))
|
||||
}
|
||||
}
|
||||
|
||||
// AddValue same as `Add` but accepts reflect.Value
|
||||
// instead.
|
||||
func (bv *Values) AddValue(values ...reflect.Value) {
|
||||
for _, v := range values {
|
||||
if !goodVal(v) {
|
||||
continue
|
||||
}
|
||||
*bv = append(*bv, v)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove unbinds a binding value based on the type,
|
||||
// it returns true if at least one field is not binded anymore.
|
||||
//
|
||||
// The "n" indicates the number of elements to remove, if <=0 then it's 1,
|
||||
// this is useful because you may have bind more than one value to two or more fields
|
||||
// with the same type.
|
||||
func (bv *Values) Remove(value interface{}, n int) bool {
|
||||
return bv.remove(reflect.TypeOf(value), n)
|
||||
}
|
||||
|
||||
func (bv *Values) remove(typ reflect.Type, n int) (ok bool) {
|
||||
input := *bv
|
||||
for i, in := range input {
|
||||
if equalTypes(in.Type(), typ) {
|
||||
ok = true
|
||||
input = input[:i+copy(input[i:], input[i+1:])]
|
||||
if n > 1 {
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
*bv = input
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Has returns true if a binder responsible to
|
||||
// bind and return a type of "typ" is already registered to this controller.
|
||||
func (bv *Values) Has(value interface{}) bool {
|
||||
return bv.valueTypeExists(reflect.TypeOf(value))
|
||||
}
|
||||
|
||||
func (bv *Values) valueTypeExists(typ reflect.Type) bool {
|
||||
input := *bv
|
||||
for _, in := range input {
|
||||
if equalTypes(in.Type(), typ) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// AddOnce binds a value to the controller's field with the same type,
|
||||
// if it's not binded already.
|
||||
//
|
||||
// Returns false if binded already or the value is not the proper one for binding,
|
||||
// otherwise true.
|
||||
func (bv *Values) AddOnce(value interface{}) bool {
|
||||
return bv.addIfNotExists(reflect.ValueOf(value))
|
||||
}
|
||||
|
||||
func (bv *Values) addIfNotExists(v reflect.Value) bool {
|
||||
var (
|
||||
typ = v.Type() // no element, raw things here.
|
||||
)
|
||||
|
||||
if !goodVal(v) {
|
||||
return false
|
||||
}
|
||||
|
||||
if bv.valueTypeExists(typ) {
|
||||
return false
|
||||
}
|
||||
|
||||
bv.AddValue(v)
|
||||
return true
|
||||
}
|
||||
105
mvc/engine.go
Normal file
105
mvc/engine.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package mvc
|
||||
|
||||
import (
|
||||
"github.com/kataras/golog"
|
||||
"github.com/kataras/iris/mvc/di"
|
||||
|
||||
"github.com/kataras/iris/context"
|
||||
"github.com/kataras/iris/core/router"
|
||||
)
|
||||
|
||||
// Engine contains the Dependencies which will be binded
|
||||
// to the controller(s) or handler(s) that can be created
|
||||
// using the Engine's `Handler` and `Controller` methods.
|
||||
//
|
||||
// This is not exported for being used by everyone, use it only when you want
|
||||
// to share engines between multi mvc.go#Application
|
||||
// or make custom mvc handlers that can be used on the standard
|
||||
// iris' APIBuilder. The last one reason is the most useful here,
|
||||
// although end-devs can use the `MakeHandler` as well.
|
||||
//
|
||||
// For a more high-level structure please take a look at the "mvc.go#Application".
|
||||
type Engine struct {
|
||||
Dependencies *di.D
|
||||
}
|
||||
|
||||
// NewEngine returns a new engine, a container for dependencies and a factory
|
||||
// for handlers and controllers, this is used internally by the `mvc#Application` structure.
|
||||
// Please take a look at the structure's documentation for more information.
|
||||
func NewEngine() *Engine {
|
||||
return &Engine{
|
||||
Dependencies: di.New().Hijack(hijacker).GoodFunc(typeChecker),
|
||||
}
|
||||
}
|
||||
|
||||
// Clone creates and returns a new engine with the parent's(current) Dependencies.
|
||||
// It copies the current "e" dependencies and returns a new engine.
|
||||
func (e *Engine) Clone() *Engine {
|
||||
child := NewEngine()
|
||||
child.Dependencies = e.Dependencies.Clone()
|
||||
return child
|
||||
}
|
||||
|
||||
// Handler accepts a "handler" function which can accept any input arguments that match
|
||||
// with the Engine's `Dependencies` and any output result; like string, int (string,int),
|
||||
// custom structs, Result(View | Response) and anything you already know that mvc implementation supports.
|
||||
// It returns a standard `iris/context.Handler` which can be used anywhere in an Iris Application,
|
||||
// as middleware or as simple route handler or subdomain's handler.
|
||||
func (e *Engine) Handler(handler interface{}) context.Handler {
|
||||
h, err := MakeHandler(handler, e.Dependencies.Values...)
|
||||
if err != nil {
|
||||
golog.Errorf("mvc handler: %v", err)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// Controller accepts a sub router and registers any custom struct
|
||||
// as controller, if struct doesn't have any compatible methods
|
||||
// neither are registered via `ControllerActivator`'s `Handle` method
|
||||
// then the controller is not registered at all.
|
||||
//
|
||||
// A Controller may have one or more methods
|
||||
// that are wrapped to a handler and registered as routes before the server ran.
|
||||
// The controller's method can accept any input argument that are previously binded
|
||||
// via the dependencies or route's path accepts dynamic path parameters.
|
||||
// The controller's fields are also bindable via the dependencies, either a
|
||||
// static value (service) or a function (dynamically) which accepts a context
|
||||
// and returns a single value (this type is being used to find the relative field or method's input argument).
|
||||
//
|
||||
// func(c *ExampleController) Get() string |
|
||||
// (string, string) |
|
||||
// (string, int) |
|
||||
// int |
|
||||
// (int, string |
|
||||
// (string, error) |
|
||||
// bool |
|
||||
// (any, bool) |
|
||||
// error |
|
||||
// (int, error) |
|
||||
// (customStruct, error) |
|
||||
// customStruct |
|
||||
// (customStruct, int) |
|
||||
// (customStruct, string) |
|
||||
// Result or (Result, error)
|
||||
// where Get is an HTTP Method func.
|
||||
//
|
||||
// Examples at: https://github.com/kataras/iris/tree/master/_examples/mvc.
|
||||
func (e *Engine) Controller(router router.Party, controller interface{}, beforeActivate ...func(*ControllerActivator)) {
|
||||
ca := newControllerActivator(router, controller, e.Dependencies)
|
||||
|
||||
// give a priority to the "beforeActivate"
|
||||
// callbacks, if any.
|
||||
for _, cb := range beforeActivate {
|
||||
cb(ca)
|
||||
}
|
||||
|
||||
// check if controller has an "BeforeActivate" function
|
||||
// which accepts the controller activator and call it.
|
||||
if activateListener, ok := controller.(interface {
|
||||
BeforeActivate(*ControllerActivator)
|
||||
}); ok {
|
||||
activateListener.BeforeActivate(ca)
|
||||
}
|
||||
|
||||
ca.activate()
|
||||
}
|
||||
22
mvc/engine_handler_test.go
Normal file
22
mvc/engine_handler_test.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package mvc_test
|
||||
|
||||
// black-box in combination with the handler_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/kataras/iris/mvc"
|
||||
)
|
||||
|
||||
func TestMvcEngineInAndHandler(t *testing.T) {
|
||||
m := NewEngine()
|
||||
m.Dependencies.Add(testBinderFuncUserStruct, testBinderService, testBinderFuncParam)
|
||||
|
||||
var (
|
||||
h1 = m.Handler(testConsumeUserHandler)
|
||||
h2 = m.Handler(testConsumeServiceHandler)
|
||||
h3 = m.Handler(testConsumeParamHandler)
|
||||
)
|
||||
|
||||
testAppWithMvcHandlers(t, h1, h2, h3)
|
||||
}
|
||||
424
mvc/func_result.go
Normal file
424
mvc/func_result.go
Normal file
@@ -0,0 +1,424 @@
|
||||
package mvc
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/structs"
|
||||
"github.com/kataras/iris/mvc/di"
|
||||
|
||||
"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 set-ed before (the most common scenario) then do a
|
||||
// simple 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 modify 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 di.IndirectValue(reflect.ValueOf(r.Data)).Kind() == reflect.Struct {
|
||||
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)
|
||||
}
|
||||
}
|
||||
273
mvc/func_result_test.go
Normal file
273
mvc/func_result_test.go
Normal file
@@ -0,0 +1,273 @@
|
||||
package mvc_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/kataras/iris"
|
||||
"github.com/kataras/iris/context"
|
||||
"github.com/kataras/iris/httptest"
|
||||
|
||||
. "github.com/kataras/iris/mvc"
|
||||
)
|
||||
|
||||
// activator/methodfunc/func_caller.go.
|
||||
// and activator/methodfunc/func_result_dispatcher.go
|
||||
|
||||
type testControllerMethodResult struct {
|
||||
Ctx context.Context
|
||||
}
|
||||
|
||||
func (c *testControllerMethodResult) Get() Result {
|
||||
return Response{
|
||||
Text: "Hello World!",
|
||||
}
|
||||
}
|
||||
|
||||
func (c *testControllerMethodResult) GetWithStatus() Response { // or Result again, no problem.
|
||||
return 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() Result {
|
||||
var err error
|
||||
if c.Ctx.URLParamExists("err") {
|
||||
err = errors.New("error here")
|
||||
}
|
||||
return 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) Result {
|
||||
failure := Response{
|
||||
Text: "thing does not exist",
|
||||
Code: iris.StatusNotFound,
|
||||
}
|
||||
|
||||
return Try(func() Result {
|
||||
// if panic because of index exceed the slice
|
||||
// then the "failure" response will be returned instead.
|
||||
return Response{Text: things[index]}
|
||||
}, failure)
|
||||
}
|
||||
|
||||
func (c *testControllerMethodResult) GetThingWithTryDefaultBy(index int) Result {
|
||||
return Try(func() Result {
|
||||
// if panic because of index exceed the slice
|
||||
// then the default failure response will be returned instead (400 bad request).
|
||||
return Response{Text: things[index]}
|
||||
})
|
||||
}
|
||||
|
||||
func TestControllerMethodResult(t *testing.T) {
|
||||
app := iris.New()
|
||||
NewEngine().Controller(app, 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 {
|
||||
Ctx context.Context
|
||||
}
|
||||
|
||||
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()
|
||||
NewEngine().Controller(app, new(testControllerMethodResultTypes))
|
||||
|
||||
e := httptest.New(t, app, httptest.LogLevel("debug"))
|
||||
|
||||
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")
|
||||
// Author's note: <-- if that fails means that the last binder called for both input args,
|
||||
// see path_param_binder.go
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (t *testControllerViewResultRespectCtxViewData) BeginRequest(ctx context.Context) {
|
||||
ctx.ViewData("name_begin", "iris_begin")
|
||||
}
|
||||
|
||||
func (t *testControllerViewResultRespectCtxViewData) EndRequest(ctx context.Context) {
|
||||
// check if data is not overridden by return 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 View {Data: }.
|
||||
Total view data: %v`, dataWritten)
|
||||
}
|
||||
|
||||
if dataWritten["name"] == nil {
|
||||
t.T.Fatalf("view data[name] is nil, Get's return View {Data: } didn't work. Total view data: %v", dataWritten)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *testControllerViewResultRespectCtxViewData) Get() Result {
|
||||
return 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()
|
||||
NewEngine().Controller(app, new(testControllerViewResultRespectCtxViewData), func(ca *ControllerActivator) {
|
||||
ca.Dependencies.Add(t)
|
||||
})
|
||||
e := httptest.New(t, app)
|
||||
|
||||
e.GET("/").Expect().Status(iris.StatusInternalServerError)
|
||||
}
|
||||
92
mvc/handler.go
Normal file
92
mvc/handler.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package mvc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/kataras/iris/mvc/di"
|
||||
"reflect"
|
||||
"runtime"
|
||||
|
||||
"github.com/kataras/golog"
|
||||
"github.com/kataras/iris/context"
|
||||
)
|
||||
|
||||
// checks if "handler" is context.Handler; func(context.Context).
|
||||
func isContextHandler(handler interface{}) (context.Handler, bool) {
|
||||
h, is := handler.(context.Handler)
|
||||
if !is {
|
||||
fh, is := handler.(func(context.Context))
|
||||
if is {
|
||||
return fh, is
|
||||
}
|
||||
}
|
||||
return h, is
|
||||
}
|
||||
|
||||
func validateHandler(handler interface{}) error {
|
||||
if typ := reflect.TypeOf(handler); !di.IsFunc(typ) {
|
||||
return fmt.Errorf("handler expected to be a kind of func but got typeof(%s)", typ.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MustMakeHandler calls the `MakeHandler` and panics on any error.
|
||||
func MustMakeHandler(handler interface{}, bindValues ...reflect.Value) context.Handler {
|
||||
h, err := MakeHandler(handler, bindValues...)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// MakeHandler accepts a "handler" function which can accept any input arguments that match
|
||||
// with the "bindValues" types and any output result, that matches the mvc types, like string, int (string,int),
|
||||
// custom structs, Result(View | Response) and anything that you already know that mvc implementation supports,
|
||||
// and returns a low-level `context/iris.Handler` which can be used anywhere in the Iris Application,
|
||||
// as middleware or as simple route handler or party handler or subdomain handler-router.
|
||||
func MakeHandler(handler interface{}, bindValues ...reflect.Value) (context.Handler, error) {
|
||||
if err := validateHandler(handler); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if h, is := isContextHandler(handler); is {
|
||||
golog.Warnf("mvc handler: you could just use the low-level API to register a context handler instead")
|
||||
return h, nil
|
||||
}
|
||||
|
||||
fn := reflect.ValueOf(handler)
|
||||
n := fn.Type().NumIn()
|
||||
|
||||
if n == 0 {
|
||||
h := func(ctx context.Context) {
|
||||
DispatchFuncResult(ctx, fn.Call(emptyIn))
|
||||
}
|
||||
|
||||
return h, nil
|
||||
}
|
||||
|
||||
s := di.MakeFuncInjector(fn, hijacker, typeChecker, bindValues...)
|
||||
if !s.Valid {
|
||||
pc := fn.Pointer()
|
||||
fpc := runtime.FuncForPC(pc)
|
||||
callerFileName, callerLineNumber := fpc.FileLine(pc)
|
||||
callerName := fpc.Name()
|
||||
|
||||
err := fmt.Errorf("input arguments length(%d) and valid binders length(%d) are not equal for typeof '%s' which is defined at %s:%d by %s",
|
||||
n, s.Length, fn.Type().String(), callerFileName, callerLineNumber, callerName)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
h := func(ctx context.Context) {
|
||||
in := make([]reflect.Value, n, n)
|
||||
|
||||
s.Inject(&in, reflect.ValueOf(ctx))
|
||||
if ctx.IsStopped() {
|
||||
return
|
||||
}
|
||||
DispatchFuncResult(ctx, fn.Call(in))
|
||||
}
|
||||
|
||||
return h, nil
|
||||
|
||||
}
|
||||
130
mvc/handler_test.go
Normal file
130
mvc/handler_test.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package mvc_test
|
||||
|
||||
// black-box
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/kataras/iris"
|
||||
"github.com/kataras/iris/httptest"
|
||||
|
||||
. "github.com/kataras/iris/mvc"
|
||||
)
|
||||
|
||||
// dynamic func
|
||||
type testUserStruct struct {
|
||||
ID int64
|
||||
Username string
|
||||
}
|
||||
|
||||
func testBinderFunc(ctx iris.Context) testUserStruct {
|
||||
id, _ := ctx.Params().GetInt64("id")
|
||||
username := ctx.Params().Get("username")
|
||||
return testUserStruct{
|
||||
ID: id,
|
||||
Username: username,
|
||||
}
|
||||
}
|
||||
|
||||
// service
|
||||
type (
|
||||
// these TestService and TestServiceImpl could be in lowercase, unexported
|
||||
// but the `Say` method should be exported however we have those exported
|
||||
// because of the controller handler test.
|
||||
TestService interface {
|
||||
Say(string) string
|
||||
}
|
||||
TestServiceImpl struct {
|
||||
prefix string
|
||||
}
|
||||
)
|
||||
|
||||
func (s *TestServiceImpl) Say(message string) string {
|
||||
return s.prefix + " " + message
|
||||
}
|
||||
|
||||
var (
|
||||
// binders, as user-defined
|
||||
testBinderFuncUserStruct = testBinderFunc
|
||||
testBinderService = &TestServiceImpl{prefix: "say"}
|
||||
testBinderFuncParam = func(ctx iris.Context) string {
|
||||
return ctx.Params().Get("param")
|
||||
}
|
||||
|
||||
// consumers
|
||||
// a context as first input arg, which is not needed to be binded manually,
|
||||
// and a user struct which is binded to the input arg by the #1 func(ctx) any binder.
|
||||
testConsumeUserHandler = func(ctx iris.Context, user testUserStruct) {
|
||||
ctx.JSON(user)
|
||||
}
|
||||
|
||||
// just one input arg, the service which is binded by the #2 service binder.
|
||||
testConsumeServiceHandler = func(service TestService) string {
|
||||
return service.Say("something")
|
||||
}
|
||||
// just one input arg, a standar string which is binded by the #3 func(ctx) any binder.
|
||||
testConsumeParamHandler = func(myParam string) string {
|
||||
return "param is: " + myParam
|
||||
}
|
||||
)
|
||||
|
||||
func TestMakeHandler(t *testing.T) {
|
||||
var (
|
||||
h1 = MustMakeHandler(testConsumeUserHandler, reflect.ValueOf(testBinderFuncUserStruct))
|
||||
h2 = MustMakeHandler(testConsumeServiceHandler, reflect.ValueOf(testBinderService))
|
||||
h3 = MustMakeHandler(testConsumeParamHandler, reflect.ValueOf(testBinderFuncParam))
|
||||
)
|
||||
|
||||
testAppWithMvcHandlers(t, h1, h2, h3)
|
||||
}
|
||||
|
||||
func testAppWithMvcHandlers(t *testing.T, h1, h2, h3 iris.Handler) {
|
||||
app := iris.New()
|
||||
app.Get("/{id:long}/{username:string}", h1)
|
||||
app.Get("/service", h2)
|
||||
app.Get("/param/{param:string}", h3)
|
||||
|
||||
expectedUser := testUserStruct{
|
||||
ID: 42,
|
||||
Username: "kataras",
|
||||
}
|
||||
|
||||
e := httptest.New(t, app)
|
||||
// 1
|
||||
e.GET(fmt.Sprintf("/%d/%s", expectedUser.ID, expectedUser.Username)).Expect().Status(httptest.StatusOK).
|
||||
JSON().Equal(expectedUser)
|
||||
// 2
|
||||
e.GET("/service").Expect().Status(httptest.StatusOK).
|
||||
Body().Equal("say something")
|
||||
// 3
|
||||
e.GET("/param/the_param_value").Expect().Status(httptest.StatusOK).
|
||||
Body().Equal("param is: the_param_value")
|
||||
}
|
||||
|
||||
// TestBindFunctionAsFunctionInputArgument tests to bind
|
||||
// a whole dynamic function based on the current context
|
||||
// as an input argument in the mvc-like handler's function.
|
||||
func TestBindFunctionAsFunctionInputArgument(t *testing.T) {
|
||||
app := iris.New()
|
||||
postsBinder := func(ctx iris.Context) func(string) string {
|
||||
return ctx.PostValue // or FormValue, the same here.
|
||||
}
|
||||
|
||||
h := MustMakeHandler(func(get func(string) string) string {
|
||||
// send the `ctx.PostValue/FormValue("username")` value
|
||||
// to the client.
|
||||
return get("username")
|
||||
},
|
||||
// bind the function binder.
|
||||
reflect.ValueOf(postsBinder))
|
||||
|
||||
app.Post("/", h)
|
||||
|
||||
e := httptest.New(t, app)
|
||||
|
||||
expectedUsername := "kataras"
|
||||
e.POST("/").WithFormField("username", expectedUsername).
|
||||
Expect().Status(iris.StatusOK).Body().Equal(expectedUsername)
|
||||
}
|
||||
83
mvc/ideas/1/main.go
Normal file
83
mvc/ideas/1/main.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/kataras/iris"
|
||||
"github.com/kataras/iris/sessions"
|
||||
|
||||
"github.com/kataras/iris/mvc"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := iris.New()
|
||||
mvc.New(app.Party("/todo")).Configure(TodoApp)
|
||||
// no let's have a clear "mvc" package without any conversions and type aliases,
|
||||
// it's one extra import path for a whole new world, it worths it.
|
||||
//
|
||||
// app.UseMVC(app.Party("/todo")).Configure(func(app *iris.MVCApplication))
|
||||
|
||||
app.Run(iris.Addr(":8080"))
|
||||
}
|
||||
|
||||
func TodoApp(app *mvc.Application) {
|
||||
// You can use normal middlewares at MVC apps of course.
|
||||
app.Router.Use(func(ctx iris.Context) {
|
||||
ctx.Application().Logger().Infof("Path: %s", ctx.Path())
|
||||
ctx.Next()
|
||||
})
|
||||
|
||||
// Add dependencies which will be binding to the controller(s),
|
||||
// can be either a function which accepts an iris.Context and returns a single value (dynamic binding)
|
||||
// or a static struct value (service).
|
||||
app.AddDependencies(
|
||||
mvc.Session(sessions.New(sessions.Config{})),
|
||||
&prefixedLogger{prefix: "DEV"},
|
||||
)
|
||||
|
||||
app.Register(new(TodoController))
|
||||
|
||||
// All dependencies of the parent *mvc.Application
|
||||
// are cloned to that new child, thefore it has access to the same session as well.
|
||||
app.NewChild(app.Router.Party("/sub")).
|
||||
Register(new(TodoSubController))
|
||||
}
|
||||
|
||||
// If controller's fields (or even its functions) expecting an interface
|
||||
// but a struct value is binded then it will check if that struct value implements
|
||||
// the interface and if true then it will bind it as expected.
|
||||
|
||||
type LoggerService interface {
|
||||
Log(string)
|
||||
}
|
||||
|
||||
type prefixedLogger struct {
|
||||
prefix string
|
||||
}
|
||||
|
||||
func (s *prefixedLogger) Log(msg string) {
|
||||
fmt.Printf("%s: %s\n", s.prefix, msg)
|
||||
}
|
||||
|
||||
type TodoController struct {
|
||||
Logger LoggerService
|
||||
|
||||
Session *sessions.Session
|
||||
}
|
||||
|
||||
func (c *TodoController) Get() string {
|
||||
count := c.Session.Increment("count", 1)
|
||||
|
||||
body := fmt.Sprintf("Hello from TodoController\nTotal visits from you: %d", count)
|
||||
c.Logger.Log(body)
|
||||
return body
|
||||
}
|
||||
|
||||
type TodoSubController struct {
|
||||
Session *sessions.Session
|
||||
}
|
||||
|
||||
func (c *TodoSubController) Get() string {
|
||||
count, _ := c.Session.GetIntDefault("count", 1)
|
||||
return fmt.Sprintf("Hello from TodoSubController.\nRead-only visits count: %d", count)
|
||||
}
|
||||
90
mvc/mvc.go
Normal file
90
mvc/mvc.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package mvc
|
||||
|
||||
import "github.com/kataras/iris/core/router"
|
||||
|
||||
// Application is the high-level compoment of the "mvc" package.
|
||||
// It's the API that you will be using to register controllers among wih their
|
||||
// dependencies that your controllers may expecting.
|
||||
// It contains the Router(iris.Party) in order to be able to register
|
||||
// template layout, middleware, done handlers as you used with the
|
||||
// standard Iris APIBuilder.
|
||||
//
|
||||
// The Engine is created by the `New` method and it's the dependencies holder
|
||||
// and controllers factory.
|
||||
//
|
||||
// See `mvc#New` for more.
|
||||
type Application struct {
|
||||
Engine *Engine
|
||||
Router router.Party
|
||||
}
|
||||
|
||||
func newApp(engine *Engine, subRouter router.Party) *Application {
|
||||
return &Application{
|
||||
Engine: engine,
|
||||
Router: subRouter,
|
||||
}
|
||||
}
|
||||
|
||||
// New returns a new mvc Application based on a "subRouter".
|
||||
// Application creates a new engine which is responsible for binding the dependencies
|
||||
// and creating and activating the app's controller(s).
|
||||
//
|
||||
// Example: `New(app.Party("/todo"))`.
|
||||
func New(subRouter router.Party) *Application {
|
||||
return newApp(NewEngine(), subRouter)
|
||||
}
|
||||
|
||||
// Configure can be used to pass one or more functions that accept this
|
||||
// Application, use this to add dependencies and controller(s).
|
||||
//
|
||||
// Example: `New(app.Party("/todo")).Configure(func(mvcApp *mvc.Application){...})`.
|
||||
func (app *Application) Configure(configurators ...func(*Application)) *Application {
|
||||
for _, c := range configurators {
|
||||
c(app)
|
||||
}
|
||||
return app
|
||||
}
|
||||
|
||||
// AddDependencies adds one or more values as dependencies.
|
||||
// The value can be a single struct value-instance or a function
|
||||
// which has one input and one output, the input should be
|
||||
// an `iris.Context` and the output can be any type, that output type
|
||||
// will be binded to the controller's field, if matching or to the
|
||||
// controller's methods, if matching.
|
||||
//
|
||||
// The dependencies can be changed per-controller as well via a `beforeActivate`
|
||||
// on the `Register` method or when the controller has the `BeforeActivate(c *ControllerActivator)`
|
||||
// method defined.
|
||||
//
|
||||
// It returns this Application.
|
||||
//
|
||||
// Example: `.AddDependencies(loggerService{prefix: "dev"}, func(ctx iris.Context) User {...})`.
|
||||
func (app *Application) AddDependencies(values ...interface{}) *Application {
|
||||
app.Engine.Dependencies.Add(values...)
|
||||
return app
|
||||
}
|
||||
|
||||
// Register adds a controller for the current Router.
|
||||
// It accept any custom struct which its functions will be transformed
|
||||
// to routes.
|
||||
//
|
||||
// The second, optional and variadic argument is the "beforeActive",
|
||||
// use that when you want to modify the controller before the activation
|
||||
// and registration to the main Iris Application.
|
||||
//
|
||||
// It returns this Application.
|
||||
//
|
||||
// Example: `.Register(new(TodoController))`.
|
||||
func (app *Application) Register(controller interface{}, beforeActivate ...func(*ControllerActivator)) *Application {
|
||||
app.Engine.Controller(app.Router, controller, beforeActivate...)
|
||||
return app
|
||||
}
|
||||
|
||||
// NewChild creates and returns a new Application which will be adapted
|
||||
// to the "subRouter", it adopts
|
||||
// the dependencies bindings from the parent(current) one.
|
||||
//
|
||||
// Example: `.NewChild(irisApp.Party("/sub")).Register(new(TodoSubController))`.
|
||||
func (app *Application) NewChild(subRouter router.Party) *Application {
|
||||
return newApp(app.Engine.Clone(), subRouter)
|
||||
}
|
||||
120
mvc/param.go
Normal file
120
mvc/param.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package mvc
|
||||
|
||||
import (
|
||||
"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"
|
||||
)
|
||||
|
||||
// for methods inside a controller.
|
||||
|
||||
func getPathParamsForInput(params []macro.TemplateParam, funcIn ...reflect.Type) (values []reflect.Value) {
|
||||
if len(funcIn) == 0 || len(params) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
consumedParams := make(map[int]bool, 0)
|
||||
for _, in := range funcIn {
|
||||
for j, p := range params {
|
||||
if _, consumed := consumedParams[j]; consumed {
|
||||
continue
|
||||
}
|
||||
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 paramType.Assignable(in.Kind()) {
|
||||
consumedParams[j] = true
|
||||
// fmt.Printf("path_param_binder.go: bind path param func for paramName = '%s' and paramType = '%s'\n", paramName, paramType.String())
|
||||
values = append(values, makeFuncParamGetter(paramType, paramName))
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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 paramType.Assignable(in.Kind()) {
|
||||
// // fmt.Printf("path_param_binder.go: bind path param func for paramName = '%s' and paramType = '%s'\n", paramName, paramType.String())
|
||||
// values = append(values, makeFuncParamGetter(paramType, paramName))
|
||||
// }
|
||||
|
||||
// funcInIdx++
|
||||
// }
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func makeFuncParamGetter(paramType ast.ParamType, paramName string) reflect.Value {
|
||||
var fn interface{}
|
||||
|
||||
switch paramType {
|
||||
case ast.ParamTypeInt:
|
||||
fn = func(ctx context.Context) int {
|
||||
v, _ := ctx.Params().GetInt(paramName)
|
||||
return v
|
||||
}
|
||||
case ast.ParamTypeLong:
|
||||
fn = func(ctx context.Context) int64 {
|
||||
v, _ := ctx.Params().GetInt64(paramName)
|
||||
return v
|
||||
}
|
||||
case ast.ParamTypeBoolean:
|
||||
fn = func(ctx context.Context) bool {
|
||||
v, _ := ctx.Params().GetBool(paramName)
|
||||
return v
|
||||
}
|
||||
default:
|
||||
// string, path...
|
||||
fn = func(ctx context.Context) string {
|
||||
return ctx.Params().Get(paramName)
|
||||
}
|
||||
}
|
||||
|
||||
return reflect.ValueOf(fn)
|
||||
}
|
||||
|
||||
// for raw handlers, independent of a controller.
|
||||
|
||||
// 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}
|
||||
}
|
||||
}
|
||||
66
mvc/param_test.go
Normal file
66
mvc/param_test.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package mvc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/kataras/iris/context"
|
||||
)
|
||||
|
||||
func TestPathParamsBinder(t *testing.T) {
|
||||
m := NewEngine()
|
||||
m.Dependencies.Add(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 := NewEngine()
|
||||
m.Dependencies.Add(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")
|
||||
}
|
||||
}
|
||||
54
mvc/reflect.go
Normal file
54
mvc/reflect.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package mvc
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/kataras/iris/context"
|
||||
"github.com/kataras/iris/mvc/di"
|
||||
)
|
||||
|
||||
var baseControllerTyp = reflect.TypeOf((*BaseController)(nil)).Elem()
|
||||
|
||||
func isBaseController(ctrlTyp reflect.Type) bool {
|
||||
return ctrlTyp.Implements(baseControllerTyp)
|
||||
}
|
||||
|
||||
var contextTyp = reflect.TypeOf((*context.Context)(nil)).Elem()
|
||||
|
||||
func isContext(inTyp reflect.Type) bool {
|
||||
return inTyp.Implements(contextTyp)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
var (
|
||||
typeChecker = func(fn reflect.Type) bool {
|
||||
// valid if that single input arg is a typeof context.Context.
|
||||
return fn.NumIn() == 1 && isContext(fn.In(0))
|
||||
}
|
||||
|
||||
hijacker = func(fieldOrFuncInput reflect.Type) (*di.BindObject, bool) {
|
||||
if !isContext(fieldOrFuncInput) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// this is being used on both func injector and struct injector.
|
||||
// if the func's input argument or the struct's field is a type of Context
|
||||
// then we can do a fast binding using the ctxValue
|
||||
// which is used as slice of reflect.Value, because of the final method's `Call`.
|
||||
return &di.BindObject{
|
||||
Type: contextTyp,
|
||||
BindType: di.Dynamic,
|
||||
ReturnValue: func(ctxValue []reflect.Value) reflect.Value {
|
||||
return ctxValue[0]
|
||||
},
|
||||
}, true
|
||||
}
|
||||
)
|
||||
17
mvc/session.go
Normal file
17
mvc/session.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package mvc
|
||||
|
||||
import (
|
||||
"github.com/kataras/iris/context"
|
||||
"github.com/kataras/iris/sessions"
|
||||
)
|
||||
|
||||
// Session -> TODO: think of move all bindings to
|
||||
// a different folder like "bindings"
|
||||
// so it will be used as .Bind(bindings.Session(manager))
|
||||
// or let it here but change the rest of the binding names as well
|
||||
// because they are not "binders", their result are binders to be percise.
|
||||
func Session(sess *sessions.Sessions) func(context.Context) *sessions.Session {
|
||||
return func(ctx context.Context) *sessions.Session {
|
||||
return sess.Start(ctx)
|
||||
}
|
||||
}
|
||||
43
mvc/session_controller.go
Normal file
43
mvc/session_controller.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package mvc
|
||||
|
||||
import (
|
||||
"github.com/kataras/iris/context"
|
||||
"github.com/kataras/iris/sessions"
|
||||
)
|
||||
|
||||
var defaultSessionManager = 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 {
|
||||
Manager *sessions.Sessions
|
||||
Session *sessions.Session
|
||||
}
|
||||
|
||||
// BeforeActivate 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) BeforeActivate(ca *ControllerActivator) {
|
||||
if didntBindManually := ca.Dependencies.AddOnce(defaultSessionManager); didntBindManually {
|
||||
ca.Router.GetReporter().Add(
|
||||
`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 initializes the current user's Session.
|
||||
func (s *SessionController) BeginRequest(ctx context.Context) {
|
||||
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)
|
||||
}
|
||||
|
||||
// EndRequest is here to complete the `BaseController`.
|
||||
func (s *SessionController) EndRequest(ctx context.Context) {}
|
||||
Reference in New Issue
Block a user