1
0
mirror of https://github.com/kataras/iris.git synced 2026-01-20 02:16:08 +00:00

Update to version 8.5.0 | NEW: MVC Output Result | Read HISTORY.md

Former-commit-id: 6a3579f2500fc715d7dc606478960946dcade61d
This commit is contained in:
Gerasimos (Makis) Maropoulos
2017-10-09 15:26:46 +03:00
parent fda35cbdb5
commit 49ee8f2d75
40 changed files with 1959 additions and 191 deletions

View File

@@ -131,9 +131,15 @@ func (t TController) HandlerOf(methodFunc methodfunc.MethodFunc) context.Handler
t.persistenceController.Handle(c)
}
// if previous (binded) handlers stoped the execution
// we should know that.
if ctx.IsStopped() {
return
}
// init the request.
b.BeginRequest(ctx)
if ctx.IsStopped() {
if ctx.IsStopped() { // if begin request stopped the execution
return
}
@@ -146,7 +152,8 @@ func (t TController) HandlerOf(methodFunc methodfunc.MethodFunc) context.Handler
t.modelController.Handle(ctx, c)
}
// finally, execute the controller, don't check for IsStopped.
// end the request, don't check for stopped because this does the actual writing
// if no response written already.
b.EndRequest(ctx)
}
}
@@ -170,8 +177,11 @@ func RegisterMethodHandlers(t TController, registerFunc RegisterFunc) {
}
// the actual method functions
// i.e for "GET" it's the `Get()`.
methods := methodfunc.Resolve(t.Type)
methods, err := methodfunc.Resolve(t.Type)
if err != nil {
golog.Errorf("MVC %s: %s", t.FullName, err.Error())
// don't stop here.
}
// range over the type info's method funcs,
// build a new handler for each of these
// methods and register them to their
@@ -181,7 +191,7 @@ func RegisterMethodHandlers(t TController, registerFunc RegisterFunc) {
for _, m := range methods {
h := t.HandlerOf(m)
if h == nil {
golog.Debugf("MVC %s: nil method handler found for %s", t.FullName, m.Name)
golog.Warnf("MVC %s: nil method handler found for %s", t.FullName, m.Name)
continue
}
registeredHandlers := append(middleware, h)

View File

@@ -173,7 +173,6 @@ func LookupFields(elem reflect.Type, matcher func(reflect.StructField) bool, han
// is easier for debugging, if ever needed.
func lookupStructField(elem reflect.Type, matcher func(reflect.StructField) bool, handler func(*Field)) *Field {
// fmt.Printf("lookup struct for elem: %s\n", elem.Name())
// ignore if that field is not a struct
if elem.Kind() != reflect.Struct {
return nil
@@ -182,6 +181,7 @@ func lookupStructField(elem reflect.Type, matcher func(reflect.StructField) bool
// search by fields.
for i, n := 0, elem.NumField(); i < n; i++ {
elemField := elem.Field(i)
if matcher(elemField) {
// we area inside the correct type.
f := &Field{

View File

@@ -11,52 +11,9 @@ import (
// to support more than one input arguments without performance cost compared to previous implementation.
// so it's hard-coded written to check the length of input args and their types.
func buildMethodCall(a *ast) func(ctx context.Context, f reflect.Value) {
// if accepts one or more parameters.
if a.dynamic {
// if one function input argument then call the function
// by "casting" (faster).
if l := len(a.paramKeys); l == 1 {
paramType := a.paramTypes[0]
paramKey := a.paramKeys[0]
if paramType == paramTypeInt {
return func(ctx context.Context, f reflect.Value) {
v, _ := ctx.Params().GetInt(paramKey)
f.Interface().(func(int))(v)
}
}
if paramType == paramTypeLong {
return func(ctx context.Context, f reflect.Value) {
v, _ := ctx.Params().GetInt64(paramKey)
f.Interface().(func(int64))(v)
}
}
if paramType == paramTypeBoolean {
return func(ctx context.Context, f reflect.Value) {
v, _ := ctx.Params().GetBool(paramKey)
f.Interface().(func(bool))(v)
}
}
// string, path...
return func(ctx context.Context, f reflect.Value) {
f.Interface().(func(string))(ctx.Params().Get(paramKey))
}
}
// if func input arguments are more than one then
// use the Call method (slower).
return func(ctx context.Context, f reflect.Value) {
f.Call(a.paramValues(ctx))
}
}
// if it's static without any receivers then just call it.
// if func input arguments are more than one then
// use the Call method (slower).
return func(ctx context.Context, f reflect.Value) {
f.Interface().(func())()
DispatchFuncResult(ctx, f.Call(a.paramValues(ctx)))
}
}

View File

@@ -51,7 +51,6 @@ func fetchInfos(typ reflect.Type) (methods []FuncInfo) {
// search the entire controller
// for any compatible method function
// and add that.
for i, n := 0, typ.NumMethod(); i < n; i++ {
m := typ.Method(i)
name := m.Name

View File

@@ -87,6 +87,22 @@ func (p *funcParser) parse() (*ast, error) {
a.relPath += "/" + strings.ToLower(w)
}
// This fixes a problem when developer misses to append the keyword `By`
// to the method function when input arguments are declared (for path parameters binding).
// We could just use it with `By` keyword but this is not a good practise
// because what happens if we will become naive and declare something like
// Get(id int) and GetBy(username string) or GetBy(id int) ? it's not working because of duplication of the path.
// Docs are clear about that but we are humans, they may do a mistake by accident but
// framework will not allow that.
// So the best thing we can do to help prevent those errors is by printing that message
// below to the developer.
// Note: it should be at the end of the words loop because a.dynamic may be true later on.
if numIn := p.info.Type.NumIn(); numIn > 1 && !a.dynamic {
return nil, fmt.Errorf("found %d input arguments but keyword 'By' is missing from '%s'",
// -1 because end-developer wants to know the actual input arguments, without the struct holder.
numIn-1, p.info.Name)
}
return a, nil
}
@@ -160,6 +176,10 @@ type ast struct {
// }
func (a *ast) paramValues(ctx context.Context) []reflect.Value {
if !a.dynamic {
return nil
}
l := len(a.paramKeys)
values := make([]reflect.Value, l, l)
for i := 0; i < l; i++ {

View File

@@ -0,0 +1,187 @@
package methodfunc
import (
"reflect"
"strings"
"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.
type Result interface {
// Dispatch should sends the response to the context's response writer.
Dispatch(ctx context.Context)
}
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) {
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) |
// customStruct |
// (customStruct, int) |
// (customStruct, string) |
// Result or (Result, error)
// where Get is an HTTP METHOD.
func DispatchFuncResult(ctx context.Context, values []reflect.Value) {
numOut := len(values)
if numOut == 0 {
return
}
var (
statusCode int
contentType string
content []byte
custom interface{}
err error
)
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 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)
}

View File

@@ -3,8 +3,8 @@ package methodfunc
import (
"reflect"
"github.com/kataras/golog"
"github.com/kataras/iris/context"
"github.com/kataras/iris/core/errors"
)
// MethodFunc the handler function.
@@ -26,15 +26,17 @@ type MethodFunc struct {
// Resolve returns all the method funcs
// necessary information and actions to
// perform the request.
func Resolve(typ reflect.Type) (methodFuncs []MethodFunc) {
func Resolve(typ reflect.Type) ([]MethodFunc, error) {
r := errors.NewReporter()
var methodFuncs []MethodFunc
infos := fetchInfos(typ)
for _, info := range infos {
parser := newFuncParser(info)
a, err := parser.parse()
if err != nil {
golog.Errorf("MVC: %s\n", err)
if r.AddErr(err) {
continue
}
methodFunc := MethodFunc{
RelPath: a.relPath,
FuncInfo: info,
@@ -44,5 +46,5 @@ func Resolve(typ reflect.Type) (methodFuncs []MethodFunc) {
methodFuncs = append(methodFuncs, methodFunc)
}
return
return methodFuncs, r.Return()
}

View File

@@ -9,6 +9,56 @@ import (
"github.com/kataras/iris/mvc/activator"
)
// C is the lightweight BaseController type as an alternative of the `Controller` struct type.
// It contains only the Name of the controller and the Context, it's the best option
// to balance the performance cost reflection uses
// if your controller uses the new func output values dispatcher feature;
// func(c *ExampleController) Get() string |
// (string, string) |
// (string, int) |
// int |
// (int, string |
// (string, error) |
// error |
// (int, error) |
// (customStruct, error) |
// customStruct |
// (customStruct, int) |
// (customStruct, string) |
// Result or (Result, error)
// where Get is an HTTP Method func.
//
// Look `core/router#APIBuilder#Controller` method too.
//
// It completes the `activator.BaseController` interface.
//
// Example at: https://github.com/kataras/iris/tree/master/_examples/mvc/using-method-result/controllers.
// Example usage at: https://github.com/kataras/iris/blob/master/mvc/method_result_test.go#L17.
type C struct {
// The Name of the `C` controller.
Name string
// The current context.Context.
//
// we have to name it for two reasons:
// 1: can't ignore these via reflection, it doesn't give an option to
// see if the functions is derived from another type.
// 2: end-developer may want to use some method functions
// or any fields that could be conflict with the context's.
Ctx context.Context
}
var _ activator.BaseController = &C{}
// SetName sets the controller's full name.
// It's called internally.
func (c *C) SetName(name string) { c.Name = name }
// BeginRequest starts the request by initializing the `Context` field.
func (c *C) BeginRequest(ctx context.Context) { c.Ctx = ctx }
// EndRequest does nothing, is here to complete the `BaseController` interface.
func (c *C) EndRequest(ctx context.Context) {}
// Controller is the base controller for the high level controllers instances.
//
// This base controller is used as an alternative way of building
@@ -61,6 +111,8 @@ import (
// Note: Binded values of context.Handler type are being recognised as middlewares by the router.
//
// Look `core/router/APIBuilder#Controller` method too.
//
// It completes the `activator.BaseController` interface.
type Controller struct {
// Name contains the current controller's full name.
//
@@ -121,6 +173,10 @@ type Controller struct {
Ctx context.Context
}
var _ activator.BaseController = &Controller{}
var ctrlSuffix = reflect.TypeOf(Controller{}).Name()
// SetName sets the controller's full name.
// It's called internally.
func (c *Controller) SetName(name string) {
@@ -238,6 +294,7 @@ func (c *Controller) BeginRequest(ctx context.Context) {
c.Params = ctx.Params()
// response status code
c.Status = ctx.GetStatusCode()
// share values
c.Values = ctx.Values()
// view data for templates, remember
@@ -251,12 +308,12 @@ func (c *Controller) BeginRequest(ctx context.Context) {
}
func (c *Controller) tryWriteHeaders() {
if status := c.Status; status > 0 && status != c.Ctx.GetStatusCode() {
c.Ctx.StatusCode(status)
if c.Status > 0 && c.Status != c.Ctx.GetStatusCode() {
c.Ctx.StatusCode(c.Status)
}
if contentType := c.ContentType; contentType != "" {
c.Ctx.ContentType(contentType)
if c.ContentType != "" {
c.Ctx.ContentType(c.ContentType)
}
}
@@ -269,7 +326,7 @@ func (c *Controller) tryWriteHeaders() {
// It's called internally.
// End-Developer can ovveride it but still should be called at the end.
func (c *Controller) EndRequest(ctx context.Context) {
if ctx.ResponseWriter().Written() > 0 {
if ctx.ResponseWriter().Written() >= 0 { // status code only (0) or actual body written(>0)
return
}
@@ -289,16 +346,10 @@ func (c *Controller) EndRequest(ctx context.Context) {
if layout := c.Layout; layout != "" {
ctx.ViewLayout(layout)
}
if data := c.Data; len(data) > 0 {
for k, v := range data {
ctx.ViewData(k, v)
}
if len(c.Data) > 0 {
ctx.Values().Set(ctx.Application().ConfigurationReadOnly().GetViewDataContextKey(), c.Data)
}
ctx.View(view)
}
}
var ctrlSuffix = reflect.TypeOf(Controller{}).Name()
var _ activator.BaseController = &Controller{}

View File

@@ -71,13 +71,13 @@ func TestControllerMethodFuncs(t *testing.T) {
e := httptest.New(t, app)
for _, method := range router.AllMethods {
e.Request(method, "/").Expect().Status(httptest.StatusOK).
e.Request(method, "/").Expect().Status(iris.StatusOK).
Body().Equal(method)
e.Request(method, "/all").Expect().Status(httptest.StatusOK).
e.Request(method, "/all").Expect().Status(iris.StatusOK).
Body().Equal(method)
e.Request(method, "/any").Expect().Status(httptest.StatusOK).
e.Request(method, "/any").Expect().Status(iris.StatusOK).
Body().Equal(method)
}
}
@@ -89,13 +89,13 @@ func TestControllerMethodAndPathHandleMany(t *testing.T) {
e := httptest.New(t, app)
for _, method := range router.AllMethods {
e.Request(method, "/").Expect().Status(httptest.StatusOK).
e.Request(method, "/").Expect().Status(iris.StatusOK).
Body().Equal(method)
e.Request(method, "/path1").Expect().Status(httptest.StatusOK).
e.Request(method, "/path1").Expect().Status(iris.StatusOK).
Body().Equal(method)
e.Request(method, "/path2").Expect().Status(httptest.StatusOK).
e.Request(method, "/path2").Expect().Status(iris.StatusOK).
Body().Equal(method)
}
}
@@ -114,7 +114,7 @@ func TestControllerPersistenceFields(t *testing.T) {
app := iris.New()
app.Controller("/", &testControllerPersistence{Data: data})
e := httptest.New(t, app)
e.GET("/").Expect().Status(httptest.StatusOK).
e.GET("/").Expect().Status(iris.StatusOK).
Body().Equal(data)
}
@@ -165,9 +165,9 @@ func TestControllerBeginAndEndRequestFunc(t *testing.T) {
doneResponse := "done"
for _, username := range usernames {
e.GET("/profile/" + username).Expect().Status(httptest.StatusOK).
e.GET("/profile/" + username).Expect().Status(iris.StatusOK).
Body().Equal(username + doneResponse)
e.POST("/profile/" + username).Expect().Status(httptest.StatusOK).
e.POST("/profile/" + username).Expect().Status(iris.StatusOK).
Body().Equal(username + doneResponse)
}
}
@@ -190,7 +190,7 @@ func TestControllerBeginAndEndRequestFuncBindMiddleware(t *testing.T) {
}
}
ctx.StatusCode(httptest.StatusForbidden)
ctx.StatusCode(iris.StatusForbidden)
ctx.Writef("forbidden")
}
@@ -203,18 +203,18 @@ func TestControllerBeginAndEndRequestFuncBindMiddleware(t *testing.T) {
for username, allow := range usernames {
getEx := e.GET("/profile/" + username).Expect()
if allow {
getEx.Status(httptest.StatusOK).
getEx.Status(iris.StatusOK).
Body().Equal(username + doneResponse)
} else {
getEx.Status(httptest.StatusForbidden).Body().Equal("forbidden")
getEx.Status(iris.StatusForbidden).Body().Equal("forbidden")
}
postEx := e.POST("/profile/" + username).Expect()
if allow {
postEx.Status(httptest.StatusOK).
postEx.Status(iris.StatusOK).
Body().Equal(username + doneResponse)
} else {
postEx.Status(httptest.StatusForbidden).Body().Equal("forbidden")
postEx.Status(iris.StatusForbidden).Body().Equal("forbidden")
}
}
}
@@ -275,7 +275,7 @@ func TestControllerModel(t *testing.T) {
}
for _, username := range usernames {
e.GET("/model/" + username).Expect().Status(httptest.StatusOK).
e.GET("/model/" + username).Expect().Status(iris.StatusOK).
Body().Equal(username + username + "2")
}
}
@@ -318,9 +318,9 @@ func TestControllerBind(t *testing.T) {
e := httptest.New(t, app)
expected := t1 + t2
e.GET("/").Expect().Status(httptest.StatusOK).
e.GET("/").Expect().Status(iris.StatusOK).
Body().Equal(expected)
e.GET("/deep").Expect().Status(httptest.StatusOK).
e.GET("/deep").Expect().Status(iris.StatusOK).
Body().Equal(expected)
}
@@ -376,7 +376,7 @@ func TestControllerRelPathAndRelTmpl(t *testing.T) {
e := httptest.New(t, app)
for path, tt := range tests {
e.GET(path).Expect().Status(httptest.StatusOK).JSON().Equal(tt)
e.GET(path).Expect().Status(iris.StatusOK).JSON().Equal(tt)
}
}
@@ -436,7 +436,7 @@ func TestControllerInsideControllerRecursively(t *testing.T) {
e := httptest.New(t, app)
e.GET("/user/" + username).Expect().
Status(httptest.StatusOK).Body().Equal(expected)
Status(iris.StatusOK).Body().Equal(expected)
}
type testControllerRelPathFromFunc struct{ mvc.Controller }
@@ -467,35 +467,35 @@ func TestControllerRelPathFromFunc(t *testing.T) {
app.Controller("/", new(testControllerRelPathFromFunc))
e := httptest.New(t, app)
e.GET("/").Expect().Status(httptest.StatusOK).
e.GET("/").Expect().Status(iris.StatusOK).
Body().Equal("GET:/")
e.GET("/42").Expect().Status(httptest.StatusOK).
e.GET("/42").Expect().Status(iris.StatusOK).
Body().Equal("GET:/42")
e.GET("/something/true").Expect().Status(httptest.StatusOK).
e.GET("/something/true").Expect().Status(iris.StatusOK).
Body().Equal("GET:/something/true")
e.GET("/something/false").Expect().Status(httptest.StatusOK).
e.GET("/something/false").Expect().Status(iris.StatusOK).
Body().Equal("GET:/something/false")
e.GET("/something/truee").Expect().Status(httptest.StatusNotFound)
e.GET("/something/falsee").Expect().Status(httptest.StatusNotFound)
e.GET("/something/kataras/42").Expect().Status(httptest.StatusOK).
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(httptest.StatusOK).
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(httptest.StatusOK).
e.GET("/something/true/else/this/42").Expect().Status(iris.StatusOK).
Body().Equal("GET:/something/true/else/this/42")
e.GET("/login").Expect().Status(httptest.StatusOK).
e.GET("/login").Expect().Status(iris.StatusOK).
Body().Equal("GET:/login")
e.POST("/login").Expect().Status(httptest.StatusOK).
e.POST("/login").Expect().Status(iris.StatusOK).
Body().Equal("POST:/login")
e.GET("/admin/login").Expect().Status(httptest.StatusOK).
e.GET("/admin/login").Expect().Status(iris.StatusOK).
Body().Equal("GET:/admin/login")
e.PUT("/something/into/this").Expect().Status(httptest.StatusOK).
e.PUT("/something/into/this").Expect().Status(iris.StatusOK).
Body().Equal("PUT:/something/into/this")
e.GET("/42").Expect().Status(httptest.StatusOK).
e.GET("/42").Expect().Status(iris.StatusOK).
Body().Equal("GET:/42")
e.GET("/anything/here").Expect().Status(httptest.StatusOK).
e.GET("/anything/here").Expect().Status(iris.StatusOK).
Body().Equal("GET:/anything/here")
}
@@ -523,8 +523,8 @@ func TestControllerActivateListener(t *testing.T) {
})
e := httptest.New(t, app)
e.GET("/").Expect().Status(httptest.StatusOK).
e.GET("/").Expect().Status(iris.StatusOK).
Body().Equal("default title")
e.GET("/manual").Expect().Status(httptest.StatusOK).
e.GET("/manual").Expect().Status(iris.StatusOK).
Body().Equal("my title")
}

View File

@@ -3,17 +3,25 @@
package mvc
import (
"html/template"
"github.com/kataras/iris/mvc/activator"
)
// ActivatePayload contains the necessary information and the ability
// to alt a controller's registration options, i.e the binder.
//
// With `ActivatePayload` the `Controller` can register custom routes
// or modify the provided values that will be binded to the
// controller later on.
//
// Look the `mvc/activator#ActivatePayload` for its implementation.
//
// A shortcut for the `mvc/activator#ActivatePayload`, useful when `OnActivate` is being used.
type ActivatePayload = activator.ActivatePayload
type (
// HTML wraps the "s" with the template.HTML
// in order to be marked as safe content, to be rendered as html and not escaped.
HTML = template.HTML
// ActivatePayload contains the necessary information and the ability
// to alt a controller's registration options, i.e the binder.
//
// With `ActivatePayload` the `Controller` can register custom routes
// or modify the provided values that will be binded to the
// controller later on.
//
// Look the `mvc/activator#ActivatePayload` for its implementation.
//
// A shortcut for the `mvc/activator#ActivatePayload`, useful when `OnActivate` is being used.
ActivatePayload = activator.ActivatePayload
)

58
mvc/method_result.go Normal file
View File

@@ -0,0 +1,58 @@
package mvc
import (
"github.com/kataras/iris/context"
"github.com/kataras/iris/mvc/activator/methodfunc"
)
// build go1.9 only(go19.go)-->
// // Result is a response dispatcher.
// // All types that complete this interface
// // can be returned as values from the method functions.
// Result = methodfunc.Result
// <--
// No, let's just copy-paste in order to go 1.8 users have this type
// easy to be used from the root mvc package,
// sometimes duplication doesn't hurt.
// 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/using-method-result.
type Result interface { // NOTE: Should be always compatible with the methodfunc.Result.
// Dispatch should sends the response to the context's response writer.
Dispatch(ctx context.Context)
}
var defaultFailureResponse = Response{Code: methodfunc.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
}

View File

@@ -0,0 +1,43 @@
package mvc
import (
"github.com/kataras/iris/context"
"github.com/kataras/iris/mvc/activator/methodfunc"
)
// 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 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
}
var _ methodfunc.Result = Response{}
// Dispatch writes the response result to the context's response writer.
func (r Response) Dispatch(ctx context.Context) {
if s := r.Text; s != "" {
r.Content = []byte(s)
}
methodfunc.DispatchCommon(ctx, r.Code, r.ContentType, r.Content, r.Object, r.Err)
}

224
mvc/method_result_test.go Normal file
View File

@@ -0,0 +1,224 @@
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 {
mvc.C
}
func (c *testControllerMethodResult) Get() mvc.Result {
return mvc.Response{
Text: "Hello World!",
}
}
func (c *testControllerMethodResult) GetWithStatus() mvc.Response { // or mvc.Result again, no problem.
return mvc.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() mvc.Result {
var err error
if c.Ctx.URLParamExists("err") {
err = errors.New("error here")
}
return mvc.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) mvc.Result {
failure := mvc.Response{
Text: "thing does not exist",
Code: iris.StatusNotFound,
}
return mvc.Try(func() mvc.Result {
// if panic because of index exceed the slice
// then the "failure" response will be returned instead.
return mvc.Response{Text: things[index]}
}, failure)
}
func (c *testControllerMethodResult) GetThingWithTryDefaultBy(index int) mvc.Result {
return mvc.Try(func() mvc.Result {
// if panic because of index exceed the slice
// then the default failure response will be returned instead (400 bad request).
return mvc.Response{Text: things[index]}
})
}
func TestControllerMethodResult(t *testing.T) {
app := iris.New()
app.Controller("/", new(testControllerMethodResult))
e := httptest.New(t, app)
e.GET("/").Expect().Status(iris.StatusOK).
Body().Equal("Hello World!")
e.GET("/with/status").Expect().Status(iris.StatusNotFound).
Body().Equal("This page doesn't exist")
e.GET("/json").Expect().Status(iris.StatusOK).
JSON().Equal(iris.Map{
"name": "Iris",
"age": 2,
})
e.GET("/json").WithQuery("err", true).Expect().
Status(iris.StatusBadRequest).
Body().Equal("error here")
e.GET("/thing/with/try/1").Expect().
Status(iris.StatusOK).
Body().Equal("thing 1")
// failure because of index exceed the slice
e.GET("/thing/with/try/3").Expect().
Status(iris.StatusNotFound).
Body().Equal("thing does not exist")
e.GET("/thing/with/try/default/3").Expect().
Status(iris.StatusBadRequest).
Body().Equal("Bad Request")
}
type testControllerMethodResultTypes struct {
mvc.Controller
}
func (c *testControllerMethodResultTypes) GetText() string {
return "text"
}
func (c *testControllerMethodResultTypes) GetStatus() int {
return iris.StatusBadGateway
}
func (c *testControllerMethodResultTypes) GetTextWithStatusOk() (string, int) {
return "OK", iris.StatusOK
}
// tests should have output arguments mixed
func (c *testControllerMethodResultTypes) GetStatusWithTextNotOkBy(first string, second string) (int, string) {
return iris.StatusForbidden, "NOT_OK_" + first + second
}
func (c *testControllerMethodResultTypes) GetTextAndContentType() (string, string) {
return "<b>text</b>", "text/html"
}
type testControllerMethodCustomResult struct {
HTML string
}
// The only one required function to make that a custom Response dispatcher.
func (r testControllerMethodCustomResult) Dispatch(ctx context.Context) {
ctx.HTML(r.HTML)
}
func (c *testControllerMethodResultTypes) GetCustomResponse() testControllerMethodCustomResult {
return testControllerMethodCustomResult{"<b>text</b>"}
}
func (c *testControllerMethodResultTypes) GetCustomResponseWithStatusOk() (testControllerMethodCustomResult, int) {
return testControllerMethodCustomResult{"<b>OK</b>"}, iris.StatusOK
}
func (c *testControllerMethodResultTypes) GetCustomResponseWithStatusNotOk() (testControllerMethodCustomResult, int) {
return testControllerMethodCustomResult{"<b>internal server error</b>"}, iris.StatusInternalServerError
}
func (c *testControllerMethodResultTypes) GetCustomStruct() testCustomStruct {
return testCustomStruct{"Iris", 2}
}
func (c *testControllerMethodResultTypes) GetCustomStructWithStatusNotOk() (testCustomStruct, int) {
return testCustomStruct{"Iris", 2}, iris.StatusInternalServerError
}
func (c *testControllerMethodResultTypes) GetCustomStructWithContentType() (testCustomStruct, string) {
return testCustomStruct{"Iris", 2}, "text/xml"
}
func (c *testControllerMethodResultTypes) GetCustomStructWithError() (s testCustomStruct, err error) {
s = testCustomStruct{"Iris", 2}
if c.Ctx.URLParamExists("err") {
err = errors.New("omit return of testCustomStruct and fire error")
}
// it should send the testCustomStruct as JSON if error is nil
// otherwise it should fire the default error(BadRequest) with the error's text.
return
}
func TestControllerMethodResultTypes(t *testing.T) {
app := iris.New()
app.Controller("/", new(testControllerMethodResultTypes))
e := httptest.New(t, app)
e.GET("/text").Expect().Status(iris.StatusOK).
Body().Equal("text")
e.GET("/status").Expect().Status(iris.StatusBadGateway)
e.GET("/text/with/status/ok").Expect().Status(iris.StatusOK).
Body().Equal("OK")
e.GET("/status/with/text/not/ok/first/second").Expect().Status(iris.StatusForbidden).
Body().Equal("NOT_OK_firstsecond")
e.GET("/text/and/content/type").Expect().Status(iris.StatusOK).
ContentType("text/html", "utf-8").
Body().Equal("<b>text</b>")
e.GET("/custom/response").Expect().Status(iris.StatusOK).
ContentType("text/html", "utf-8").
Body().Equal("<b>text</b>")
e.GET("/custom/response/with/status/ok").Expect().Status(iris.StatusOK).
ContentType("text/html", "utf-8").
Body().Equal("<b>OK</b>")
e.GET("/custom/response/with/status/not/ok").Expect().Status(iris.StatusInternalServerError).
ContentType("text/html", "utf-8").
Body().Equal("<b>internal server error</b>")
expectedResultFromCustomStruct := map[string]interface{}{
"name": "Iris",
"age": 2,
}
e.GET("/custom/struct").Expect().Status(iris.StatusOK).
JSON().Equal(expectedResultFromCustomStruct)
e.GET("/custom/struct/with/status/not/ok").Expect().Status(iris.StatusInternalServerError).
JSON().Equal(expectedResultFromCustomStruct)
e.GET("/custom/struct/with/content/type").Expect().Status(iris.StatusOK).
ContentType("text/xml", "utf-8")
e.GET("/custom/struct/with/error").Expect().Status(iris.StatusOK).
JSON().Equal(expectedResultFromCustomStruct)
e.GET("/custom/struct/with/error").WithQuery("err", true).Expect().
Status(iris.StatusBadRequest). // the default status code if error is not nil
// the content should be not JSON it should be the status code's text
// it will fire the error's text
Body().Equal("omit return of testCustomStruct and fire error")
}

77
mvc/method_result_view.go Normal file
View File

@@ -0,0 +1,77 @@
package mvc
import (
"strings"
"github.com/kataras/iris/context"
"github.com/kataras/iris/mvc/activator/methodfunc"
)
// 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/using-method-result/controllers/hello_controller.go.
type View struct {
Name string
Layout string
Data interface{} // map or a custom struct.
Code int
Err error
}
var _ methodfunc.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 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 = methodfunc.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 {
ctx.Values().Set(
ctx.Application().ConfigurationReadOnly().GetViewDataContextKey(),
r.Data,
)
}
ctx.View(r.Name)
}
}