From a8a2dffc5918b6439e36d9117c96efb4e1d041ca Mon Sep 17 00:00:00 2001 From: chesedo Date: Fri, 6 Jan 2017 10:11:08 +0200 Subject: [PATCH 01/32] Have Required validator trim strings to fix #2361 This will cause the Required validator not to consider fields that has only spaces or new lines to be regarded as valid. This is done by checking if the trimmed version of the string is valid. --- validation/validation_test.go | 6 ++++++ validation/validators.go | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/validation/validation_test.go b/validation/validation_test.go index ec65b6d0..d8e880df 100644 --- a/validation/validation_test.go +++ b/validation/validation_test.go @@ -35,6 +35,12 @@ func TestRequired(t *testing.T) { if valid.Required("", "string").Ok { t.Error("\"'\" string should be false") } + if valid.Required(" ", "string").Ok { + t.Error("\" \" string should be false") // For #2361 + } + if valid.Required("\n", "string").Ok { + t.Error("new line string should be false") // For #2361 + } if !valid.Required("astaxie", "string").Ok { t.Error("string should be true") } diff --git a/validation/validators.go b/validation/validators.go index 9b04c5ce..78b10373 100644 --- a/validation/validators.go +++ b/validation/validators.go @@ -18,6 +18,7 @@ import ( "fmt" "reflect" "regexp" + "strings" "time" "unicode/utf8" ) @@ -98,7 +99,7 @@ func (r Required) IsSatisfied(obj interface{}) bool { } if str, ok := obj.(string); ok { - return len(str) > 0 + return len(strings.TrimSpace(str)) > 0 } if _, ok := obj.(bool); ok { return true From 9aedb4d05a70937533ff8cf657b365a80257406d Mon Sep 17 00:00:00 2001 From: eyalpost Date: Fri, 21 Apr 2017 15:26:41 +0300 Subject: [PATCH 02/32] phase #1 --- controller.go | 2 + param/methodparams.go | 101 ++++++++++++++++++++++++++++++++++++++++++ param/options.go | 15 +++++++ param/parsers.go | 41 +++++++++++++++++ response/renderer.go | 27 +++++++++++ response/responses.go | 44 ++++++++++++++++++ router.go | 42 ++++++++++++++---- 7 files changed, 264 insertions(+), 8 deletions(-) create mode 100644 param/methodparams.go create mode 100644 param/options.go create mode 100644 param/parsers.go create mode 100644 response/renderer.go create mode 100644 response/responses.go diff --git a/controller.go b/controller.go index c2a327b3..02e45738 100644 --- a/controller.go +++ b/controller.go @@ -28,6 +28,7 @@ import ( "strings" "github.com/astaxie/beego/context" + "github.com/astaxie/beego/param" "github.com/astaxie/beego/session" ) @@ -51,6 +52,7 @@ type ControllerComments struct { Router string AllowHTTPMethods []string Params []map[string]string + MethodParams []*param.MethodParam } // Controller defines some basic http request handler operations, such as diff --git a/param/methodparams.go b/param/methodparams.go new file mode 100644 index 00000000..2e6e6840 --- /dev/null +++ b/param/methodparams.go @@ -0,0 +1,101 @@ +package param + +import ( + "fmt" + "reflect" + + beecontext "github.com/astaxie/beego/context" +) + +//Keeps param information to be auto passed to controller methods +type MethodParam struct { + name string + parser paramParser + location paramLocation + required bool + defValue interface{} +} + +type paramLocation byte + +const ( + param paramLocation = iota + body + header +) + +type MethodParamOption func(*MethodParam) + +func Bool(name string, opts ...MethodParamOption) *MethodParam { + return newParam(name, boolParser{}, opts) +} + +func String(name string, opts ...MethodParamOption) *MethodParam { + return newParam(name, stringParser{}, opts) +} + +func Int(name string, opts ...MethodParamOption) *MethodParam { + return newParam(name, intParser{}, opts) +} + +func newParam(name string, parser paramParser, opts []MethodParamOption) (param *MethodParam) { + param = &MethodParam{name: name, parser: parser} + for _, option := range opts { + option(param) + } + return +} + +func ConvertParams(methodParams []*MethodParam, methodType reflect.Type, ctx *beecontext.Context) (result []reflect.Value) { + result = make([]reflect.Value, 0, len(methodParams)) + i := 0 + for _, p := range methodParams { + var strValue string + var value interface{} + switch p.location { + case body: + strValue = string(ctx.Input.RequestBody) + case header: + strValue = ctx.Input.Header(p.name) + default: + strValue = ctx.Input.Query(p.name) + } + + if strValue == "" { + if p.required { + ctx.Abort(400, "Missing argument "+p.name) + } else if p.defValue != nil { + value = p.defValue + } else { + value = p.parser.zeroValue() + } + } else { + var err error + value, err = p.parser.parse(strValue) + if err != nil { + //TODO: handle err + } + } + reflectValue, err := safeConvert(reflect.ValueOf(value), methodType.In(i)) + if err != nil { + //TODO: handle err + } + result = append(result, reflectValue) + i++ + } + return +} + +func safeConvert(value reflect.Value, t reflect.Type) (result reflect.Value, err error) { + defer func() { + if r := recover(); r != nil { + var ok bool + err, ok = r.(error) + if !ok { + err = fmt.Errorf("%v", r) + } + } + }() + result = value.Convert(t) + return +} diff --git a/param/options.go b/param/options.go new file mode 100644 index 00000000..91d01ac3 --- /dev/null +++ b/param/options.go @@ -0,0 +1,15 @@ +package param + +var InHeader MethodParamOption = func(p *MethodParam) { + p.location = header +} + +var IsRequired MethodParamOption = func(p *MethodParam) { + p.required = true +} + +func Default(defValue interface{}) MethodParamOption { + return func(p *MethodParam) { + p.defValue = defValue + } +} diff --git a/param/parsers.go b/param/parsers.go new file mode 100644 index 00000000..eec0bb79 --- /dev/null +++ b/param/parsers.go @@ -0,0 +1,41 @@ +package param + +import "strconv" + +type paramParser interface { + parse(value string) (interface{}, error) + zeroValue() interface{} +} + +type boolParser struct { +} + +func (p boolParser) parse(value string) (interface{}, error) { + return strconv.ParseBool(value) +} + +func (p boolParser) zeroValue() interface{} { + return false +} + +type stringParser struct { +} + +func (p stringParser) parse(value string) (interface{}, error) { + return value, nil +} + +func (p stringParser) zeroValue() interface{} { + return "" +} + +type intParser struct { +} + +func (p intParser) parse(value string) (interface{}, error) { + return strconv.Atoi(value) +} + +func (p intParser) zeroValue() interface{} { + return 0 +} diff --git a/response/renderer.go b/response/renderer.go new file mode 100644 index 00000000..ca5936db --- /dev/null +++ b/response/renderer.go @@ -0,0 +1,27 @@ +package response + +import ( + "strconv" + + beecontext "github.com/astaxie/beego/context" +) + +type Renderer interface { + Render(ctx *beecontext.Context) +} + +type rendererFunc func(ctx *beecontext.Context) + +func (f rendererFunc) Render(ctx *beecontext.Context) { + f(ctx) +} + +type StatusCode int + +func (s StatusCode) Error() string { + return strconv.Itoa(int(s)) +} + +func (s StatusCode) Render(ctx *beecontext.Context) { + ctx.Output.SetStatus(int(s)) +} diff --git a/response/responses.go b/response/responses.go new file mode 100644 index 00000000..aa042a35 --- /dev/null +++ b/response/responses.go @@ -0,0 +1,44 @@ +package response + +import ( + beecontext "github.com/astaxie/beego/context" +) + +func Json(value interface{}, encoding ...bool) Renderer { + return rendererFunc(func(ctx *beecontext.Context) { + var ( + hasIndent = true + hasEncoding = false + ) + //TODO: need access to BConfig :( + // if BConfig.RunMode == PROD { + // hasIndent = false + // } + if len(encoding) > 0 && encoding[0] { + hasEncoding = true + } + ctx.Output.JSON(value, hasIndent, hasEncoding) + }) +} + +func Error(err error) Renderer { + return rendererFunc(func(ctx *beecontext.Context) { + ctx.Output.SetStatus(500) + ctx.WriteString(err.Error()) + }) +} + +func RenderMethodResult(result interface{}, ctx *beecontext.Context) { + if result != nil { + renderer, ok := result.(Renderer) + if !ok { + err, ok := result.(error) + if ok { + renderer = Error(err) + } else { + renderer = Json(result) + } + } + renderer.Render(ctx) + } +} diff --git a/router.go b/router.go index 9f573f26..4937354a 100644 --- a/router.go +++ b/router.go @@ -29,6 +29,8 @@ import ( beecontext "github.com/astaxie/beego/context" "github.com/astaxie/beego/logs" + "github.com/astaxie/beego/param" + "github.com/astaxie/beego/response" "github.com/astaxie/beego/toolbox" "github.com/astaxie/beego/utils" ) @@ -116,6 +118,7 @@ type controllerInfo struct { handler http.Handler runFunction FilterFunc routerType int + methodParams []*param.MethodParam } // ControllerRegister containers registered router rules, controller handlers and filters. @@ -151,6 +154,10 @@ func NewControllerRegister() *ControllerRegister { // Add("/api",&RestController{},"get,post:ApiFunc" // Add("/simple",&SimpleController{},"get:GetFunc;post:PostFunc") func (p *ControllerRegister) Add(pattern string, c ControllerInterface, mappingMethods ...string) { + p.addWithMethodParams(pattern, c, nil, mappingMethods...) +} + +func (p *ControllerRegister) addWithMethodParams(pattern string, c ControllerInterface, methodParams []*param.MethodParam, mappingMethods ...string) { reflectVal := reflect.ValueOf(c) t := reflect.Indirect(reflectVal).Type() methods := make(map[string]string) @@ -181,6 +188,7 @@ func (p *ControllerRegister) Add(pattern string, c ControllerInterface, mappingM route.methods = methods route.routerType = routerTypeBeego route.controllerType = t + route.methodParams = methodParams if len(methods) == 0 { for _, m := range HTTPMETHOD { p.addToRouter(m, pattern, route) @@ -247,7 +255,7 @@ func (p *ControllerRegister) Include(cList ...ControllerInterface) { key := t.PkgPath() + ":" + t.Name() if comm, ok := GlobalControllerRouter[key]; ok { for _, a := range comm { - p.Add(a.Router, c, strings.Join(a.AllowHTTPMethods, ",")+":"+a.Method) + p.addWithMethodParams(a.Router, c, a.MethodParams, strings.Join(a.AllowHTTPMethods, ",")+":"+a.Method) } } } @@ -626,11 +634,12 @@ func (p *ControllerRegister) execFilter(context *beecontext.Context, urlPath str func (p *ControllerRegister) ServeHTTP(rw http.ResponseWriter, r *http.Request) { startTime := time.Now() var ( - runRouter reflect.Type - findRouter bool - runMethod string - routerInfo *controllerInfo - isRunnable bool + runRouter reflect.Type + findRouter bool + runMethod string + methodParams []*param.MethodParam + routerInfo *controllerInfo + isRunnable bool ) context := p.pool.Get().(*beecontext.Context) context.Reset(rw, r) @@ -742,6 +751,7 @@ func (p *ControllerRegister) ServeHTTP(rw http.ResponseWriter, r *http.Request) routerInfo.handler.ServeHTTP(rw, r) } else { runRouter = routerInfo.controllerType + methodParams = routerInfo.methodParams method := r.Method if r.Method == "POST" && context.Input.Query("_method") == "PUT" { method = "PUT" @@ -804,9 +814,14 @@ func (p *ControllerRegister) ServeHTTP(rw http.ResponseWriter, r *http.Request) execController.Options() default: if !execController.HandlerFunc(runMethod) { - var in []reflect.Value method := vc.MethodByName(runMethod) - method.Call(in) + var in []reflect.Value = param.ConvertParams(methodParams, method.Type(), context) + out := method.Call(in) + + //For backward compatibility we only handle response if we had incoming methodParams + if methodParams != nil { + p.handleParamResponse(context, execController, out) + } } } @@ -886,6 +901,17 @@ Admin: } } +func (p *ControllerRegister) handleParamResponse(context *beecontext.Context, execController ControllerInterface, results []reflect.Value) { + //looping in reverse order for the case when both error and value are returned and error sets the response status code + for i := len(results) - 1; i >= 0; i-- { + result := results[i] + if !result.IsNil() { + resultValue := result.Interface() + response.RenderMethodResult(resultValue, context) + } + } +} + // FindRouter Find Router info for URL func (p *ControllerRegister) FindRouter(context *beecontext.Context) (routerInfo *controllerInfo, isFind bool) { var urlPath = context.Input.URL() From 89e01d125c274d636f4c7eeb545be38bdefef139 Mon Sep 17 00:00:00 2001 From: eyalpost Date: Sun, 23 Apr 2017 01:33:50 +0300 Subject: [PATCH 03/32] all types implemented --- param/methodparams.go | 82 ++++++++++++++++++++++++++----------------- param/options.go | 18 +++++++--- param/parsers.go | 58 +++++++++++++++++++++--------- response/renderer.go | 9 +++++ response/responses.go | 10 ++++-- 5 files changed, 123 insertions(+), 54 deletions(-) diff --git a/param/methodparams.go b/param/methodparams.go index 2e6e6840..9cd2b1d6 100644 --- a/param/methodparams.go +++ b/param/methodparams.go @@ -5,6 +5,7 @@ import ( "reflect" beecontext "github.com/astaxie/beego/context" + "github.com/astaxie/beego/logs" ) //Keeps param information to be auto passed to controller methods @@ -13,7 +14,7 @@ type MethodParam struct { parser paramParser location paramLocation required bool - defValue interface{} + defValue string } type paramLocation byte @@ -38,6 +39,18 @@ func Int(name string, opts ...MethodParamOption) *MethodParam { return newParam(name, intParser{}, opts) } +func Float(name string, opts ...MethodParamOption) *MethodParam { + return newParam(name, floatParser{}, opts) +} + +func Time(name string, opts ...MethodParamOption) *MethodParam { + return newParam(name, timeParser{}, opts) +} + +func Json(name string, opts ...MethodParamOption) *MethodParam { + return newParam(name, jsonParser{}, opts) +} + func newParam(name string, parser paramParser, opts []MethodParamOption) (param *MethodParam) { param = &MethodParam{name: name, parser: parser} for _, option := range opts { @@ -46,42 +59,47 @@ func newParam(name string, parser paramParser, opts []MethodParamOption) (param return } -func ConvertParams(methodParams []*MethodParam, methodType reflect.Type, ctx *beecontext.Context) (result []reflect.Value) { - result = make([]reflect.Value, 0, len(methodParams)) - i := 0 - for _, p := range methodParams { - var strValue string - var value interface{} - switch p.location { - case body: - strValue = string(ctx.Input.RequestBody) - case header: - strValue = ctx.Input.Header(p.name) - default: - strValue = ctx.Input.Query(p.name) +func convertParam(param *MethodParam, paramType reflect.Type, ctx *beecontext.Context) (result reflect.Value) { + var strValue string + var reflectValue reflect.Value + switch param.location { + case body: + strValue = string(ctx.Input.RequestBody) + case header: + strValue = ctx.Input.Header(param.name) + default: + strValue = ctx.Input.Query(param.name) + } + + if strValue == "" { + if param.required { + ctx.Abort(400, fmt.Sprintf("Missing parameter %s", param.name)) + } else { + strValue = param.defValue + } + } + if strValue == "" { + reflectValue = reflect.Zero(paramType) + } else { + value, err := param.parser.parse(strValue, paramType) + if err != nil { + logs.Debug(fmt.Sprintf("Error converting param %s to type %s. Value: %s, Parser: %s, Error: %s", param.name, paramType.Name(), strValue, reflect.TypeOf(param.parser).Name(), err)) + ctx.Abort(400, fmt.Sprintf("Invalid parameter %s. Can not convert %s to type %s", param.name, strValue, paramType.Name())) } - if strValue == "" { - if p.required { - ctx.Abort(400, "Missing argument "+p.name) - } else if p.defValue != nil { - value = p.defValue - } else { - value = p.parser.zeroValue() - } - } else { - var err error - value, err = p.parser.parse(strValue) - if err != nil { - //TODO: handle err - } - } - reflectValue, err := safeConvert(reflect.ValueOf(value), methodType.In(i)) + reflectValue, err = safeConvert(reflect.ValueOf(value), paramType) if err != nil { - //TODO: handle err + panic(err) } + } + return reflectValue +} + +func ConvertParams(methodParams []*MethodParam, methodType reflect.Type, ctx *beecontext.Context) (result []reflect.Value) { + result = make([]reflect.Value, 0, len(methodParams)) + for i := 0; i < len(methodParams); i++ { + reflectValue := convertParam(methodParams[i], methodType.In(i), ctx) result = append(result, reflectValue) - i++ } return } diff --git a/param/options.go b/param/options.go index 91d01ac3..6fe10108 100644 --- a/param/options.go +++ b/param/options.go @@ -1,15 +1,25 @@ package param -var InHeader MethodParamOption = func(p *MethodParam) { - p.location = header -} +import ( + "fmt" +) var IsRequired MethodParamOption = func(p *MethodParam) { p.required = true } +var InHeader MethodParamOption = func(p *MethodParam) { + p.location = header +} + +var InBody MethodParamOption = func(p *MethodParam) { + p.location = body +} + func Default(defValue interface{}) MethodParamOption { return func(p *MethodParam) { - p.defValue = defValue + if defValue != nil { + p.defValue = fmt.Sprintf("%v", defValue) + } } } diff --git a/param/parsers.go b/param/parsers.go index eec0bb79..e3032e20 100644 --- a/param/parsers.go +++ b/param/parsers.go @@ -1,41 +1,67 @@ package param -import "strconv" +import ( + "encoding/json" + "reflect" + "strconv" + "time" +) type paramParser interface { - parse(value string) (interface{}, error) - zeroValue() interface{} + parse(value string, toType reflect.Type) (interface{}, error) } type boolParser struct { } -func (p boolParser) parse(value string) (interface{}, error) { +func (p boolParser) parse(value string, toType reflect.Type) (interface{}, error) { return strconv.ParseBool(value) } -func (p boolParser) zeroValue() interface{} { - return false -} - type stringParser struct { } -func (p stringParser) parse(value string) (interface{}, error) { +func (p stringParser) parse(value string, toType reflect.Type) (interface{}, error) { return value, nil } -func (p stringParser) zeroValue() interface{} { - return "" -} - type intParser struct { } -func (p intParser) parse(value string) (interface{}, error) { +func (p intParser) parse(value string, toType reflect.Type) (interface{}, error) { return strconv.Atoi(value) } -func (p intParser) zeroValue() interface{} { - return 0 +type floatParser struct { +} + +func (p floatParser) parse(value string, toType reflect.Type) (interface{}, error) { + if toType.Kind() == reflect.Float32 { + return strconv.ParseFloat(value, 32) + } + return strconv.ParseFloat(value, 64) +} + +type timeParser struct { +} + +func (p timeParser) parse(value string, toType reflect.Type) (result interface{}, err error) { + result, err = time.Parse(time.RFC3339, value) + if err != nil { + result, err = time.Parse("2006-01-02", value) + } + return +} + +type jsonParser struct { +} + +func (p jsonParser) parse(value string, toType reflect.Type) (interface{}, error) { + pResult := reflect.New(toType) + v := pResult.Interface() + err := json.Unmarshal([]byte(value), v) + if err != nil { + return nil, err + } + return pResult.Elem().Interface(), nil } diff --git a/response/renderer.go b/response/renderer.go index ca5936db..f5f9a52d 100644 --- a/response/renderer.go +++ b/response/renderer.go @@ -25,3 +25,12 @@ func (s StatusCode) Error() string { func (s StatusCode) Render(ctx *beecontext.Context) { ctx.Output.SetStatus(int(s)) } + +type statusCodeWithRender struct { + statusCode int + rendererFunc +} + +func (s statusCodeWithRender) Error() string { + return strconv.Itoa(s.statusCode) +} diff --git a/response/responses.go b/response/responses.go index aa042a35..5fbe4be1 100644 --- a/response/responses.go +++ b/response/responses.go @@ -21,20 +21,26 @@ func Json(value interface{}, encoding ...bool) Renderer { }) } -func Error(err error) Renderer { +func errorRenderer(err error) Renderer { return rendererFunc(func(ctx *beecontext.Context) { ctx.Output.SetStatus(500) ctx.WriteString(err.Error()) }) } +func Redirect(localurl string) statusCodeWithRender { + return statusCodeWithRender{302, func(ctx *beecontext.Context) { + ctx.Redirect(302, localurl) + }} +} + func RenderMethodResult(result interface{}, ctx *beecontext.Context) { if result != nil { renderer, ok := result.(Renderer) if !ok { err, ok := result.(error) if ok { - renderer = Error(err) + renderer = errorRenderer(err) } else { renderer = Json(result) } From 19f4a6ac0b51cc685fc966b7e8ca12fd89e5bc43 Mon Sep 17 00:00:00 2001 From: Eyal Post Date: Sun, 23 Apr 2017 21:37:09 +0300 Subject: [PATCH 04/32] slice support --- param/conv.go | 68 ++++++++++++++++++++++++++++++++++++++++ param/methodparams.go | 72 +++---------------------------------------- param/parsers.go | 29 ++++++++++++++++- router.go | 2 +- 4 files changed, 102 insertions(+), 69 deletions(-) create mode 100644 param/conv.go diff --git a/param/conv.go b/param/conv.go new file mode 100644 index 00000000..e909c4ea --- /dev/null +++ b/param/conv.go @@ -0,0 +1,68 @@ +package param + +import ( + "fmt" + "reflect" + + beecontext "github.com/astaxie/beego/context" + "github.com/astaxie/beego/logs" +) + +func convertParam(param *MethodParam, paramType reflect.Type, ctx *beecontext.Context) (result reflect.Value) { + var strValue string + var reflectValue reflect.Value + switch param.location { + case body: + strValue = string(ctx.Input.RequestBody) + case header: + strValue = ctx.Input.Header(param.name) + default: + strValue = ctx.Input.Query(param.name) + } + + if strValue == "" { + if param.required { + ctx.Abort(400, fmt.Sprintf("Missing parameter %s", param.name)) + } else { + strValue = param.defValue + } + } + if strValue == "" { + reflectValue = reflect.Zero(paramType) + } else { + value, err := param.parser.parse(strValue, paramType) + if err != nil { + logs.Debug(fmt.Sprintf("Error converting param %s to type %s. Value: %s, Error: %s", param.name, paramType, strValue, err)) + ctx.Abort(400, fmt.Sprintf("Invalid parameter %s. Can not convert %s to type %s", param.name, strValue, paramType)) + } + + reflectValue, err = safeConvert(reflect.ValueOf(value), paramType) + if err != nil { + panic(err) + } + } + return reflectValue +} + +func ConvertParams(methodParams []*MethodParam, methodType reflect.Type, ctx *beecontext.Context) (result []reflect.Value) { + result = make([]reflect.Value, 0, len(methodParams)) + for i := 0; i < len(methodParams); i++ { + reflectValue := convertParam(methodParams[i], methodType.In(i), ctx) + result = append(result, reflectValue) + } + return +} + +func safeConvert(value reflect.Value, t reflect.Type) (result reflect.Value, err error) { + defer func() { + if r := recover(); r != nil { + var ok bool + err, ok = r.(error) + if !ok { + err = fmt.Errorf("%v", r) + } + } + }() + result = value.Convert(t) + return +} diff --git a/param/methodparams.go b/param/methodparams.go index 9cd2b1d6..a3e25a55 100644 --- a/param/methodparams.go +++ b/param/methodparams.go @@ -1,13 +1,5 @@ package param -import ( - "fmt" - "reflect" - - beecontext "github.com/astaxie/beego/context" - "github.com/astaxie/beego/logs" -) - //Keeps param information to be auto passed to controller methods type MethodParam struct { name string @@ -51,6 +43,11 @@ func Json(name string, opts ...MethodParamOption) *MethodParam { return newParam(name, jsonParser{}, opts) } +func AsSlice(param *MethodParam) *MethodParam { + param.parser = sliceParser(param.parser) + return param +} + func newParam(name string, parser paramParser, opts []MethodParamOption) (param *MethodParam) { param = &MethodParam{name: name, parser: parser} for _, option := range opts { @@ -58,62 +55,3 @@ func newParam(name string, parser paramParser, opts []MethodParamOption) (param } return } - -func convertParam(param *MethodParam, paramType reflect.Type, ctx *beecontext.Context) (result reflect.Value) { - var strValue string - var reflectValue reflect.Value - switch param.location { - case body: - strValue = string(ctx.Input.RequestBody) - case header: - strValue = ctx.Input.Header(param.name) - default: - strValue = ctx.Input.Query(param.name) - } - - if strValue == "" { - if param.required { - ctx.Abort(400, fmt.Sprintf("Missing parameter %s", param.name)) - } else { - strValue = param.defValue - } - } - if strValue == "" { - reflectValue = reflect.Zero(paramType) - } else { - value, err := param.parser.parse(strValue, paramType) - if err != nil { - logs.Debug(fmt.Sprintf("Error converting param %s to type %s. Value: %s, Parser: %s, Error: %s", param.name, paramType.Name(), strValue, reflect.TypeOf(param.parser).Name(), err)) - ctx.Abort(400, fmt.Sprintf("Invalid parameter %s. Can not convert %s to type %s", param.name, strValue, paramType.Name())) - } - - reflectValue, err = safeConvert(reflect.ValueOf(value), paramType) - if err != nil { - panic(err) - } - } - return reflectValue -} - -func ConvertParams(methodParams []*MethodParam, methodType reflect.Type, ctx *beecontext.Context) (result []reflect.Value) { - result = make([]reflect.Value, 0, len(methodParams)) - for i := 0; i < len(methodParams); i++ { - reflectValue := convertParam(methodParams[i], methodType.In(i), ctx) - result = append(result, reflectValue) - } - return -} - -func safeConvert(value reflect.Value, t reflect.Type) (result reflect.Value, err error) { - defer func() { - if r := recover(); r != nil { - var ok bool - err, ok = r.(error) - if !ok { - err = fmt.Errorf("%v", r) - } - } - }() - result = value.Convert(t) - return -} diff --git a/param/parsers.go b/param/parsers.go index e3032e20..cfa1a981 100644 --- a/param/parsers.go +++ b/param/parsers.go @@ -4,6 +4,7 @@ import ( "encoding/json" "reflect" "strconv" + "strings" "time" ) @@ -11,6 +12,12 @@ type paramParser interface { parse(value string, toType reflect.Type) (interface{}, error) } +type parserFunc func(value string, toType reflect.Type) (interface{}, error) + +func (f parserFunc) parse(value string, toType reflect.Type) (interface{}, error) { + return f(value, toType) +} + type boolParser struct { } @@ -37,7 +44,11 @@ type floatParser struct { func (p floatParser) parse(value string, toType reflect.Type) (interface{}, error) { if toType.Kind() == reflect.Float32 { - return strconv.ParseFloat(value, 32) + res, err := strconv.ParseFloat(value, 32) + if err != nil { + return nil, err + } + return float32(res), nil } return strconv.ParseFloat(value, 64) } @@ -65,3 +76,19 @@ func (p jsonParser) parse(value string, toType reflect.Type) (interface{}, error } return pResult.Elem().Interface(), nil } + +func sliceParser(elemParser paramParser) paramParser { + return parserFunc(func(value string, toType reflect.Type) (interface{}, error) { + values := strings.Split(value, ",") + result := reflect.MakeSlice(toType, 0, len(values)) + elemType := toType.Elem() + for _, v := range values { + parsedValue, err := elemParser.parse(v, elemType) + if err != nil { + return nil, err + } + result = reflect.Append(result, reflect.ValueOf(parsedValue)) + } + return result.Interface(), nil + }) +} diff --git a/router.go b/router.go index 4937354a..be69419c 100644 --- a/router.go +++ b/router.go @@ -905,7 +905,7 @@ func (p *ControllerRegister) handleParamResponse(context *beecontext.Context, ex //looping in reverse order for the case when both error and value are returned and error sets the response status code for i := len(results) - 1; i >= 0; i-- { result := results[i] - if !result.IsNil() { + if result.Kind() != reflect.Interface || !result.IsNil() { resultValue := result.Interface() response.RenderMethodResult(resultValue, context) } From 864693d2f840374460950e3f393ee48e5b8e868f Mon Sep 17 00:00:00 2001 From: eyalpost Date: Mon, 24 Apr 2017 02:35:04 +0300 Subject: [PATCH 05/32] mall fixes --- cache/conv.go | 2 +- param/options.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cache/conv.go b/cache/conv.go index dbdff1c7..87800586 100644 --- a/cache/conv.go +++ b/cache/conv.go @@ -28,7 +28,7 @@ func GetString(v interface{}) string { return string(result) default: if v != nil { - return fmt.Sprintf("%v", result) + return fmt.Sprint(result) } } return "" diff --git a/param/options.go b/param/options.go index 6fe10108..0692f9d1 100644 --- a/param/options.go +++ b/param/options.go @@ -19,7 +19,7 @@ var InBody MethodParamOption = func(p *MethodParam) { func Default(defValue interface{}) MethodParamOption { return func(p *MethodParam) { if defValue != nil { - p.defValue = fmt.Sprintf("%v", defValue) + p.defValue = fmt.Sprint(defValue) } } } From 9b79437778cf2b78f00d71c963cfc4732a01b39e Mon Sep 17 00:00:00 2001 From: eyalpost Date: Tue, 25 Apr 2017 16:00:49 +0300 Subject: [PATCH 06/32] all types working + controller comments generation --- param/conv.go | 58 +++++++----- param/methodparams.go | 72 ++++++++------- param/options.go | 6 ++ param/parsers.go | 57 ++++++++++++ parser.go | 209 +++++++++++++++++++++++++++++++++++------- router.go | 3 + 6 files changed, 317 insertions(+), 88 deletions(-) diff --git a/param/conv.go b/param/conv.go index e909c4ea..4938f45f 100644 --- a/param/conv.go +++ b/param/conv.go @@ -9,39 +9,51 @@ import ( ) func convertParam(param *MethodParam, paramType reflect.Type, ctx *beecontext.Context) (result reflect.Value) { - var strValue string - var reflectValue reflect.Value - switch param.location { - case body: - strValue = string(ctx.Input.RequestBody) - case header: - strValue = ctx.Input.Header(param.name) - default: - strValue = ctx.Input.Query(param.name) - } - - if strValue == "" { + paramValue := getParamValue(param, ctx) + if paramValue == "" { if param.required { ctx.Abort(400, fmt.Sprintf("Missing parameter %s", param.name)) } else { - strValue = param.defValue + paramValue = param.defValue } } - if strValue == "" { - reflectValue = reflect.Zero(paramType) + + reflectValue, err := parseValue(paramValue, paramType) + if err != nil { + logs.Debug(fmt.Sprintf("Error converting param %s to type %s. Value: %v, Error: %s", param.name, paramType, paramValue, err)) + ctx.Abort(400, fmt.Sprintf("Invalid parameter %s. Can not convert %v to type %s", param.name, paramValue, paramType)) + } + + return reflectValue +} + +func getParamValue(param *MethodParam, ctx *beecontext.Context) string { + switch param.location { + case body: + return string(ctx.Input.RequestBody) + case header: + return ctx.Input.Header(param.name) + // if strValue == "" && strings.Contains(param.name, "_") { //magically handle X-Headers? + // strValue = ctx.Input.Header(strings.Replace(param.name, "_", "-", -1)) + // } + case path: + return ctx.Input.Query(":" + param.name) + default: + return ctx.Input.Query(param.name) + } +} + +func parseValue(paramValue string, paramType reflect.Type) (result reflect.Value, err error) { + if paramValue == "" { + return reflect.Zero(paramType), nil } else { - value, err := param.parser.parse(strValue, paramType) + value, err := parse(paramValue, paramType) if err != nil { - logs.Debug(fmt.Sprintf("Error converting param %s to type %s. Value: %s, Error: %s", param.name, paramType, strValue, err)) - ctx.Abort(400, fmt.Sprintf("Invalid parameter %s. Can not convert %s to type %s", param.name, strValue, paramType)) + return result, err } - reflectValue, err = safeConvert(reflect.ValueOf(value), paramType) - if err != nil { - panic(err) - } + return safeConvert(reflect.ValueOf(value), paramType) } - return reflectValue } func ConvertParams(methodParams []*MethodParam, methodType reflect.Type, ctx *beecontext.Context) (result []reflect.Value) { diff --git a/param/methodparams.go b/param/methodparams.go index a3e25a55..23fd6661 100644 --- a/param/methodparams.go +++ b/param/methodparams.go @@ -1,9 +1,13 @@ package param +import ( + "fmt" + "strings" +) + //Keeps param information to be auto passed to controller methods type MethodParam struct { name string - parser paramParser location paramLocation required bool defValue string @@ -13,45 +17,51 @@ type paramLocation byte const ( param paramLocation = iota + path body header ) -type MethodParamOption func(*MethodParam) - -func Bool(name string, opts ...MethodParamOption) *MethodParam { - return newParam(name, boolParser{}, opts) -} - -func String(name string, opts ...MethodParamOption) *MethodParam { - return newParam(name, stringParser{}, opts) -} - -func Int(name string, opts ...MethodParamOption) *MethodParam { - return newParam(name, intParser{}, opts) -} - -func Float(name string, opts ...MethodParamOption) *MethodParam { - return newParam(name, floatParser{}, opts) -} - -func Time(name string, opts ...MethodParamOption) *MethodParam { - return newParam(name, timeParser{}, opts) -} - -func Json(name string, opts ...MethodParamOption) *MethodParam { - return newParam(name, jsonParser{}, opts) -} - -func AsSlice(param *MethodParam) *MethodParam { - param.parser = sliceParser(param.parser) - return param +func New(name string, opts ...MethodParamOption) *MethodParam { + return newParam(name, nil, opts) } func newParam(name string, parser paramParser, opts []MethodParamOption) (param *MethodParam) { - param = &MethodParam{name: name, parser: parser} + param = &MethodParam{name: name} for _, option := range opts { option(param) } return } + +func Make(list ...*MethodParam) []*MethodParam { + if len(list) > 0 { + return list + } + return nil +} + +func (mp *MethodParam) String() string { + options := []string{} + result := "param.New(\"" + mp.name + "\"" + if mp.required { + options = append(options, "param.IsRequired") + } + switch mp.location { + case path: + options = append(options, "param.InPath") + case body: + options = append(options, "param.InBody") + case header: + options = append(options, "param.InHeader") + } + if mp.defValue != "" { + options = append(options, fmt.Sprintf(`param.Default("%s")`, mp.defValue)) + } + if len(options) > 0 { + result += ", " + } + result += strings.Join(options, ", ") + result += ")" + return result +} diff --git a/param/options.go b/param/options.go index 0692f9d1..0013c31e 100644 --- a/param/options.go +++ b/param/options.go @@ -4,6 +4,8 @@ import ( "fmt" ) +type MethodParamOption func(*MethodParam) + var IsRequired MethodParamOption = func(p *MethodParam) { p.required = true } @@ -12,6 +14,10 @@ var InHeader MethodParamOption = func(p *MethodParam) { p.location = header } +var InPath MethodParamOption = func(p *MethodParam) { + p.location = path +} + var InBody MethodParamOption = func(p *MethodParam) { p.location = body } diff --git a/param/parsers.go b/param/parsers.go index cfa1a981..64cabe49 100644 --- a/param/parsers.go +++ b/param/parsers.go @@ -12,6 +12,45 @@ type paramParser interface { parse(value string, toType reflect.Type) (interface{}, error) } +func parse(value string, t reflect.Type) (interface{}, error) { + parser := getParser(t) + return parser.parse(value, t) +} + +func getParser(t reflect.Type) paramParser { + switch t.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return intParser{} + case reflect.Slice: + if t.Elem().Kind() == reflect.Uint8 { //treat []byte as string + return stringParser{} + } + elemParser := getParser(t.Elem()) + if elemParser == (jsonParser{}) { + return elemParser + } + return sliceParser(elemParser) + case reflect.Bool: + return boolParser{} + case reflect.String: + return stringParser{} + case reflect.Float32, reflect.Float64: + return floatParser{} + case reflect.Ptr: + elemParser := getParser(t.Elem()) + if elemParser == (jsonParser{}) { + return elemParser + } + return ptrParser(elemParser) + default: + if t.PkgPath() == "time" && t.Name() == "Time" { + return timeParser{} + } + return jsonParser{} + } +} + type parserFunc func(value string, toType reflect.Type) (interface{}, error) func (f parserFunc) parse(value string, toType reflect.Type) (interface{}, error) { @@ -92,3 +131,21 @@ func sliceParser(elemParser paramParser) paramParser { return result.Interface(), nil }) } + +func ptrParser(elemParser paramParser) paramParser { + return parserFunc(func(value string, toType reflect.Type) (interface{}, error) { + parsedValue, err := elemParser.parse(value, toType.Elem()) + if err != nil { + return nil, err + } + newValPtr := reflect.New(toType.Elem()) + newVal := reflect.Indirect(newValPtr) + convertedVal, err := safeConvert(reflect.ValueOf(parsedValue), toType.Elem()) + if err != nil { + return nil, err + } + + newVal.Set(convertedVal) + return newValPtr.Interface(), nil + }) +} diff --git a/parser.go b/parser.go index d40ee3ce..43e052bd 100644 --- a/parser.go +++ b/parser.go @@ -24,10 +24,14 @@ import ( "io/ioutil" "os" "path/filepath" + "regexp" "sort" + "strconv" "strings" + "unicode" "github.com/astaxie/beego/logs" + "github.com/astaxie/beego/param" "github.com/astaxie/beego/utils" ) @@ -35,6 +39,7 @@ var globalRouterTemplate = `package routers import ( "github.com/astaxie/beego" + "github.com/astaxie/beego/param" ) func init() { @@ -81,7 +86,7 @@ func parserPkg(pkgRealpath, pkgpath string) error { if specDecl.Recv != nil { exp, ok := specDecl.Recv.List[0].Type.(*ast.StarExpr) // Check that the type is correct first beforing throwing to parser if ok { - parserComments(specDecl.Doc, specDecl.Name.String(), fmt.Sprint(exp.X), pkgpath) + parserComments(specDecl, fmt.Sprint(exp.X), pkgpath) } } } @@ -93,44 +98,168 @@ func parserPkg(pkgRealpath, pkgpath string) error { return nil } -func parserComments(comments *ast.CommentGroup, funcName, controllerName, pkgpath string) error { - if comments != nil && comments.List != nil { - for _, c := range comments.List { - t := strings.TrimSpace(strings.TrimLeft(c.Text, "//")) - if strings.HasPrefix(t, "@router") { - elements := strings.TrimLeft(t, "@router ") - e1 := strings.SplitN(elements, " ", 2) - if len(e1) < 1 { - return errors.New("you should has router information") - } - key := pkgpath + ":" + controllerName - cc := ControllerComments{} - cc.Method = funcName - cc.Router = e1[0] - if len(e1) == 2 && e1[1] != "" { - e1 = strings.SplitN(e1[1], " ", 2) - if len(e1) >= 1 { - cc.AllowHTTPMethods = strings.Split(strings.Trim(e1[0], "[]"), ",") - } else { - cc.AllowHTTPMethods = append(cc.AllowHTTPMethods, "get") - } - } else { - cc.AllowHTTPMethods = append(cc.AllowHTTPMethods, "get") - } - if len(e1) == 2 && e1[1] != "" { - keyval := strings.Split(strings.Trim(e1[1], "[]"), " ") - for _, kv := range keyval { - kk := strings.Split(kv, ":") - cc.Params = append(cc.Params, map[string]string{strings.Join(kk[:len(kk)-1], ":"): kk[len(kk)-1]}) - } - } - genInfoList[key] = append(genInfoList[key], cc) - } +type parsedComment struct { + routerPath string + methods []string + params map[string]parsedParam +} + +type parsedParam struct { + name string + datatype string + location string + defValue string + required bool +} + +func parserComments(f *ast.FuncDecl, controllerName, pkgpath string) error { + if f.Doc != nil { + parsedComment, err := parseComment(f.Doc.List) + if err != nil { + return err } + key := pkgpath + ":" + controllerName + cc := ControllerComments{} + cc.Method = f.Name.String() + cc.Router = parsedComment.routerPath + cc.AllowHTTPMethods = parsedComment.methods + cc.MethodParams = buildMethodParams(f.Type.Params.List, parsedComment) + genInfoList[key] = append(genInfoList[key], cc) + } return nil } +func buildMethodParams(funcParams []*ast.Field, pc *parsedComment) []*param.MethodParam { + result := make([]*param.MethodParam, 0, len(funcParams)) + for _, fparam := range funcParams { + methodParam := buildMethodParam(fparam, pc) + result = append(result, methodParam) + } + return result +} + +func buildMethodParam(fparam *ast.Field, pc *parsedComment) *param.MethodParam { + options := []param.MethodParamOption{} + name := fparam.Names[0].Name + if cparam, ok := pc.params[name]; ok { + //Build param from comment info + name = cparam.name + if cparam.required { + options = append(options, param.IsRequired) + } + switch cparam.location { + case "body": + options = append(options, param.InBody) + case "header": + options = append(options, param.InHeader) + case "path": + options = append(options, param.InPath) + } + if cparam.defValue != "" { + options = append(options, param.Default(cparam.defValue)) + } + } else { + if paramInPath(name, pc.routerPath) { + options = append(options, param.InPath) + } + } + return param.New(name, options...) +} + +func paramInPath(name, route string) bool { + return strings.HasSuffix(route, ":"+name) || + strings.Contains(route, ":"+name+"/") +} + +var routeRegex = regexp.MustCompile(`@router\s+(\S+)(?:\s+\[(\S+)\])?`) + +func parseComment(lines []*ast.Comment) (pc *parsedComment, err error) { + pc = &parsedComment{} + for _, c := range lines { + t := strings.TrimSpace(strings.TrimLeft(c.Text, "//")) + if strings.HasPrefix(t, "@router") { + matches := routeRegex.FindStringSubmatch(t) + if len(matches) == 3 { + pc.routerPath = matches[1] + methods := matches[2] + if methods == "" { + pc.methods = []string{"get"} + //pc.hasGet = true + } else { + pc.methods = strings.Split(methods, ",") + //pc.hasGet = strings.Contains(methods, "get") + } + } else { + return nil, errors.New("Router information is missing") + } + } else if strings.HasPrefix(t, "@Param") { + pv := getparams(strings.TrimSpace(strings.TrimLeft(t, "@Param"))) + if len(pv) < 4 { + logs.Error("Invalid @Param format. Needs at least 4 parameters") + } + p := parsedParam{} + names := strings.Split(pv[0], "=") + funcParamName := names[0] + if len(names) > 1 { + p.name = names[1] + } else { + p.name = funcParamName + } + p.location = pv[1] + p.datatype = pv[2] + switch len(pv) { + case 5: + p.required, _ = strconv.ParseBool(pv[3]) + case 6: + p.defValue = pv[3] + p.required, _ = strconv.ParseBool(pv[4]) + } + if pc.params == nil { + pc.params = map[string]parsedParam{} + } + pc.params[funcParamName] = p + } + } + return +} + +// direct copy from bee\g_docs.go +// analisys params return []string +// @Param query form string true "The email for login" +// [query form string true "The email for login"] +func getparams(str string) []string { + var s []rune + var j int + var start bool + var r []string + var quoted int8 + for _, c := range []rune(str) { + if unicode.IsSpace(c) && quoted == 0 { + if !start { + continue + } else { + start = false + j++ + r = append(r, string(s)) + s = make([]rune, 0) + continue + } + } + + start = true + if c == '"' { + quoted ^= 1 + continue + } + s = append(s, c) + } + if len(s) > 0 { + r = append(r, string(s)) + } + return r +} + func genRouterCode(pkgRealpath string) { os.Mkdir(getRouterDir(pkgRealpath), 0755) logs.Info("generate router from comments") @@ -163,12 +292,24 @@ func genRouterCode(pkgRealpath string) { } params = strings.TrimRight(params, ",") + "}" } + methodParams := "param.Make(" + if len(c.MethodParams) > 0 { + lines := make([]string, 0, len(c.MethodParams)) + for _, m := range c.MethodParams { + lines = append(lines, fmt.Sprint(m)) + } + methodParams += "\n " + + strings.Join(lines, ",\n ") + + ",\n " + } + methodParams += ")" globalinfo = globalinfo + ` beego.GlobalControllerRouter["` + k + `"] = append(beego.GlobalControllerRouter["` + k + `"], beego.ControllerComments{ Method: "` + strings.TrimSpace(c.Method) + `", ` + "Router: `" + c.Router + "`" + `, AllowHTTPMethods: ` + allmethod + `, + MethodParams: ` + methodParams + `, Params: ` + params + `}) ` } diff --git a/router.go b/router.go index 57479248..546db22d 100644 --- a/router.go +++ b/router.go @@ -908,6 +908,9 @@ func (p *ControllerRegister) handleParamResponse(context *beecontext.Context, ex response.RenderMethodResult(resultValue, context) } } + if !context.ResponseWriter.Started && context.Output.Status == 0 { + context.Output.SetStatus(200) + } } // FindRouter Find Router info for URL From cbd831042a34af663669a2fb4cce2f0ab78e7064 Mon Sep 17 00:00:00 2001 From: eyalpost Date: Tue, 25 Apr 2017 18:39:42 +0300 Subject: [PATCH 07/32] move under context --- {param => context/param}/conv.go | 0 {param => context/param}/methodparams.go | 0 {param => context/param}/options.go | 0 {param => context/param}/parsers.go | 0 {response => context/response}/renderer.go | 0 {response => context/response}/responses.go | 0 controller.go | 2 +- parser.go | 4 ++-- router.go | 4 ++-- 9 files changed, 5 insertions(+), 5 deletions(-) rename {param => context/param}/conv.go (100%) rename {param => context/param}/methodparams.go (100%) rename {param => context/param}/options.go (100%) rename {param => context/param}/parsers.go (100%) rename {response => context/response}/renderer.go (100%) rename {response => context/response}/responses.go (100%) diff --git a/param/conv.go b/context/param/conv.go similarity index 100% rename from param/conv.go rename to context/param/conv.go diff --git a/param/methodparams.go b/context/param/methodparams.go similarity index 100% rename from param/methodparams.go rename to context/param/methodparams.go diff --git a/param/options.go b/context/param/options.go similarity index 100% rename from param/options.go rename to context/param/options.go diff --git a/param/parsers.go b/context/param/parsers.go similarity index 100% rename from param/parsers.go rename to context/param/parsers.go diff --git a/response/renderer.go b/context/response/renderer.go similarity index 100% rename from response/renderer.go rename to context/response/renderer.go diff --git a/response/responses.go b/context/response/responses.go similarity index 100% rename from response/responses.go rename to context/response/responses.go diff --git a/controller.go b/controller.go index 02e45738..510e16b8 100644 --- a/controller.go +++ b/controller.go @@ -28,7 +28,7 @@ import ( "strings" "github.com/astaxie/beego/context" - "github.com/astaxie/beego/param" + "github.com/astaxie/beego/context/param" "github.com/astaxie/beego/session" ) diff --git a/parser.go b/parser.go index 43e052bd..17d9a5d2 100644 --- a/parser.go +++ b/parser.go @@ -30,8 +30,8 @@ import ( "strings" "unicode" + "github.com/astaxie/beego/context/param" "github.com/astaxie/beego/logs" - "github.com/astaxie/beego/param" "github.com/astaxie/beego/utils" ) @@ -39,7 +39,7 @@ var globalRouterTemplate = `package routers import ( "github.com/astaxie/beego" - "github.com/astaxie/beego/param" + "github.com/astaxie/beego/context/param" ) func init() { diff --git a/router.go b/router.go index 546db22d..ebe70e72 100644 --- a/router.go +++ b/router.go @@ -27,9 +27,9 @@ import ( "time" beecontext "github.com/astaxie/beego/context" + "github.com/astaxie/beego/context/param" + "github.com/astaxie/beego/context/response" "github.com/astaxie/beego/logs" - "github.com/astaxie/beego/param" - "github.com/astaxie/beego/response" "github.com/astaxie/beego/toolbox" "github.com/astaxie/beego/utils" ) From 4cba78afd948470826c0bed5eda222f9824b8da7 Mon Sep 17 00:00:00 2001 From: eyalpost Date: Tue, 25 Apr 2017 23:42:35 +0300 Subject: [PATCH 08/32] small fixes --- context/param/conv.go | 25 +++++++++++++------------ context/param/parsers.go | 14 ++++++-------- parser.go | 9 ++++----- 3 files changed, 23 insertions(+), 25 deletions(-) diff --git a/context/param/conv.go b/context/param/conv.go index 4938f45f..8e580c2e 100644 --- a/context/param/conv.go +++ b/context/param/conv.go @@ -8,6 +8,15 @@ import ( "github.com/astaxie/beego/logs" ) +func ConvertParams(methodParams []*MethodParam, methodType reflect.Type, ctx *beecontext.Context) (result []reflect.Value) { + result = make([]reflect.Value, 0, len(methodParams)) + for i := 0; i < len(methodParams); i++ { + reflectValue := convertParam(methodParams[i], methodType.In(i), ctx) + result = append(result, reflectValue) + } + return +} + func convertParam(param *MethodParam, paramType reflect.Type, ctx *beecontext.Context) (result reflect.Value) { paramValue := getParamValue(param, ctx) if paramValue == "" { @@ -18,7 +27,7 @@ func convertParam(param *MethodParam, paramType reflect.Type, ctx *beecontext.Co } } - reflectValue, err := parseValue(paramValue, paramType) + reflectValue, err := parseValue(param, paramValue, paramType) if err != nil { logs.Debug(fmt.Sprintf("Error converting param %s to type %s. Value: %v, Error: %s", param.name, paramType, paramValue, err)) ctx.Abort(400, fmt.Sprintf("Invalid parameter %s. Can not convert %v to type %s", param.name, paramValue, paramType)) @@ -43,11 +52,12 @@ func getParamValue(param *MethodParam, ctx *beecontext.Context) string { } } -func parseValue(paramValue string, paramType reflect.Type) (result reflect.Value, err error) { +func parseValue(param *MethodParam, paramValue string, paramType reflect.Type) (result reflect.Value, err error) { if paramValue == "" { return reflect.Zero(paramType), nil } else { - value, err := parse(paramValue, paramType) + parser := getParser(param, paramType) + value, err := parser.parse(paramValue, paramType) if err != nil { return result, err } @@ -56,15 +66,6 @@ func parseValue(paramValue string, paramType reflect.Type) (result reflect.Value } } -func ConvertParams(methodParams []*MethodParam, methodType reflect.Type, ctx *beecontext.Context) (result []reflect.Value) { - result = make([]reflect.Value, 0, len(methodParams)) - for i := 0; i < len(methodParams); i++ { - reflectValue := convertParam(methodParams[i], methodType.In(i), ctx) - result = append(result, reflectValue) - } - return -} - func safeConvert(value reflect.Value, t reflect.Type) (result reflect.Value, err error) { defer func() { if r := recover(); r != nil { diff --git a/context/param/parsers.go b/context/param/parsers.go index 64cabe49..2b48e878 100644 --- a/context/param/parsers.go +++ b/context/param/parsers.go @@ -12,12 +12,7 @@ type paramParser interface { parse(value string, toType reflect.Type) (interface{}, error) } -func parse(value string, t reflect.Type) (interface{}, error) { - parser := getParser(t) - return parser.parse(value, t) -} - -func getParser(t reflect.Type) paramParser { +func getParser(param *MethodParam, t reflect.Type) paramParser { switch t.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: @@ -26,7 +21,10 @@ func getParser(t reflect.Type) paramParser { if t.Elem().Kind() == reflect.Uint8 { //treat []byte as string return stringParser{} } - elemParser := getParser(t.Elem()) + if param.location == body { + return jsonParser{} + } + elemParser := getParser(param, t.Elem()) if elemParser == (jsonParser{}) { return elemParser } @@ -38,7 +36,7 @@ func getParser(t reflect.Type) paramParser { case reflect.Float32, reflect.Float64: return floatParser{} case reflect.Ptr: - elemParser := getParser(t.Elem()) + elemParser := getParser(param, t.Elem()) if elemParser == (jsonParser{}) { return elemParser } diff --git a/parser.go b/parser.go index 17d9a5d2..66f51d0b 100644 --- a/parser.go +++ b/parser.go @@ -199,12 +199,11 @@ func parseComment(lines []*ast.Comment) (pc *parsedComment, err error) { logs.Error("Invalid @Param format. Needs at least 4 parameters") } p := parsedParam{} - names := strings.Split(pv[0], "=") - funcParamName := names[0] + names := strings.SplitN(pv[0], "=>", 2) + p.name = names[0] + funcParamName := p.name if len(names) > 1 { - p.name = names[1] - } else { - p.name = funcParamName + funcParamName = names[1] } p.location = pv[1] p.datatype = pv[2] From a1bc94e648dbb04b02a06ddfd76aef3006ba52c9 Mon Sep 17 00:00:00 2001 From: eyalpost Date: Wed, 26 Apr 2017 01:00:25 +0300 Subject: [PATCH 09/32] dont generate comment if router not found --- parser.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/parser.go b/parser.go index 66f51d0b..037d5376 100644 --- a/parser.go +++ b/parser.go @@ -118,13 +118,15 @@ func parserComments(f *ast.FuncDecl, controllerName, pkgpath string) error { if err != nil { return err } - key := pkgpath + ":" + controllerName - cc := ControllerComments{} - cc.Method = f.Name.String() - cc.Router = parsedComment.routerPath - cc.AllowHTTPMethods = parsedComment.methods - cc.MethodParams = buildMethodParams(f.Type.Params.List, parsedComment) - genInfoList[key] = append(genInfoList[key], cc) + if parsedComment.routerPath != "" { + key := pkgpath + ":" + controllerName + cc := ControllerComments{} + cc.Method = f.Name.String() + cc.Router = parsedComment.routerPath + cc.AllowHTTPMethods = parsedComment.methods + cc.MethodParams = buildMethodParams(f.Type.Params.List, parsedComment) + genInfoList[key] = append(genInfoList[key], cc) + } } return nil From 1b8f05cef15029e184036c081b9d5b9542d1ecbe Mon Sep 17 00:00:00 2001 From: Eyal Post Date: Sun, 30 Apr 2017 19:28:26 +0300 Subject: [PATCH 10/32] golint fixes --- context/param/conv.go | 16 ++++++++-------- context/param/methodparams.go | 4 +++- context/param/options.go | 6 ++++++ context/response/renderer.go | 3 +++ context/response/responses.go | 9 ++++++--- router.go | 2 +- 6 files changed, 27 insertions(+), 13 deletions(-) diff --git a/context/param/conv.go b/context/param/conv.go index 8e580c2e..e961b4ad 100644 --- a/context/param/conv.go +++ b/context/param/conv.go @@ -8,6 +8,7 @@ import ( "github.com/astaxie/beego/logs" ) +// ConvertParams converts http method params to values that will be passed to the method controller as arguments func ConvertParams(methodParams []*MethodParam, methodType reflect.Type, ctx *beecontext.Context) (result []reflect.Value) { result = make([]reflect.Value, 0, len(methodParams)) for i := 0; i < len(methodParams); i++ { @@ -55,15 +56,14 @@ func getParamValue(param *MethodParam, ctx *beecontext.Context) string { func parseValue(param *MethodParam, paramValue string, paramType reflect.Type) (result reflect.Value, err error) { if paramValue == "" { return reflect.Zero(paramType), nil - } else { - parser := getParser(param, paramType) - value, err := parser.parse(paramValue, paramType) - if err != nil { - return result, err - } - - return safeConvert(reflect.ValueOf(value), paramType) } + parser := getParser(param, paramType) + value, err := parser.parse(paramValue, paramType) + if err != nil { + return result, err + } + + return safeConvert(reflect.ValueOf(value), paramType) } func safeConvert(value reflect.Value, t reflect.Type) (result reflect.Value, err error) { diff --git a/context/param/methodparams.go b/context/param/methodparams.go index 23fd6661..28772504 100644 --- a/context/param/methodparams.go +++ b/context/param/methodparams.go @@ -5,7 +5,7 @@ import ( "strings" ) -//Keeps param information to be auto passed to controller methods +//MethodParam keeps param information to be auto passed to controller methods type MethodParam struct { name string location paramLocation @@ -22,6 +22,7 @@ const ( header ) +//New creates a new MethodParam with name and specific options func New(name string, opts ...MethodParamOption) *MethodParam { return newParam(name, nil, opts) } @@ -34,6 +35,7 @@ func newParam(name string, parser paramParser, opts []MethodParamOption) (param return } +//Make creates an array of MethodParmas or an empty array func Make(list ...*MethodParam) []*MethodParam { if len(list) > 0 { return list diff --git a/context/param/options.go b/context/param/options.go index 0013c31e..846a59f6 100644 --- a/context/param/options.go +++ b/context/param/options.go @@ -4,24 +4,30 @@ import ( "fmt" ) +// MethodParamOption defines a func which apply options on a MethodParam type MethodParamOption func(*MethodParam) +// IsRequired indicates that this param is required and can not be ommited from the http request var IsRequired MethodParamOption = func(p *MethodParam) { p.required = true } +// InHeader indicates that this param is passed via an http header var InHeader MethodParamOption = func(p *MethodParam) { p.location = header } +// InPath indicates that this param is part of the URL path var InPath MethodParamOption = func(p *MethodParam) { p.location = path } +// InBody indicates that this param is passed as an http request body var InBody MethodParamOption = func(p *MethodParam) { p.location = body } +// Default provides a default value for the http param func Default(defValue interface{}) MethodParamOption { return func(p *MethodParam) { if defValue != nil { diff --git a/context/response/renderer.go b/context/response/renderer.go index f5f9a52d..2a4d2797 100644 --- a/context/response/renderer.go +++ b/context/response/renderer.go @@ -6,6 +6,7 @@ import ( beecontext "github.com/astaxie/beego/context" ) +// Renderer defines an http response renderer type Renderer interface { Render(ctx *beecontext.Context) } @@ -16,12 +17,14 @@ func (f rendererFunc) Render(ctx *beecontext.Context) { f(ctx) } +// StatusCode sets the http response status code type StatusCode int func (s StatusCode) Error() string { return strconv.Itoa(int(s)) } +// Render sets the http status code func (s StatusCode) Render(ctx *beecontext.Context) { ctx.Output.SetStatus(int(s)) } diff --git a/context/response/responses.go b/context/response/responses.go index 5fbe4be1..c033edf7 100644 --- a/context/response/responses.go +++ b/context/response/responses.go @@ -4,7 +4,8 @@ import ( beecontext "github.com/astaxie/beego/context" ) -func Json(value interface{}, encoding ...bool) Renderer { +// JSON renders value to the response as JSON +func JSON(value interface{}, encoding ...bool) Renderer { return rendererFunc(func(ctx *beecontext.Context) { var ( hasIndent = true @@ -28,12 +29,14 @@ func errorRenderer(err error) Renderer { }) } -func Redirect(localurl string) statusCodeWithRender { +// Redirect renders http 302 with a URL +func Redirect(localurl string) Renderer { return statusCodeWithRender{302, func(ctx *beecontext.Context) { ctx.Redirect(302, localurl) }} } +// RenderMethodResult renders the return value of a controller method to the output func RenderMethodResult(result interface{}, ctx *beecontext.Context) { if result != nil { renderer, ok := result.(Renderer) @@ -42,7 +45,7 @@ func RenderMethodResult(result interface{}, ctx *beecontext.Context) { if ok { renderer = errorRenderer(err) } else { - renderer = Json(result) + renderer = JSON(result) } } renderer.Render(ctx) diff --git a/router.go b/router.go index ebe70e72..33096039 100644 --- a/router.go +++ b/router.go @@ -813,7 +813,7 @@ func (p *ControllerRegister) ServeHTTP(rw http.ResponseWriter, r *http.Request) default: if !execController.HandlerFunc(runMethod) { method := vc.MethodByName(runMethod) - var in []reflect.Value = param.ConvertParams(methodParams, method.Type(), context) + in := param.ConvertParams(methodParams, method.Type(), context) out := method.Call(in) //For backward compatibility we only handle response if we had incoming methodParams From d3a16dca85d1c4ceae415134718411c7758689fc Mon Sep 17 00:00:00 2001 From: Eyal Post Date: Mon, 1 May 2017 08:57:57 +0300 Subject: [PATCH 11/32] Redirect should returns error --- context/response/responses.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/context/response/responses.go b/context/response/responses.go index c033edf7..10d1ed3f 100644 --- a/context/response/responses.go +++ b/context/response/responses.go @@ -30,7 +30,7 @@ func errorRenderer(err error) Renderer { } // Redirect renders http 302 with a URL -func Redirect(localurl string) Renderer { +func Redirect(localurl string) error { return statusCodeWithRender{302, func(ctx *beecontext.Context) { ctx.Redirect(302, localurl) }} From b2e7720fcd26a82d3a00d9846c26ba5931ca75b8 Mon Sep 17 00:00:00 2001 From: Yang Luo Date: Thu, 4 May 2017 14:02:21 +0800 Subject: [PATCH 12/32] Add an authorization plugin that supports ACL, RBAC based on casbin. It requires the built-in HTTP basic authentication by default. --- .travis.yml | 2 + plugins/authz/authz.go | 86 ++++++++++++++++++++++++++ plugins/authz/authz_model.conf | 14 +++++ plugins/authz/authz_policy.csv | 7 +++ plugins/authz/authz_test.go | 107 +++++++++++++++++++++++++++++++++ 5 files changed, 216 insertions(+) create mode 100644 plugins/authz/authz.go create mode 100644 plugins/authz/authz_model.conf create mode 100644 plugins/authz/authz_policy.csv create mode 100644 plugins/authz/authz_test.go diff --git a/.travis.yml b/.travis.yml index 479d70ca..aa88a44d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,6 +33,8 @@ install: - go get github.com/ssdb/gossdb/ssdb - go get github.com/cloudflare/golz4 - go get github.com/gogo/protobuf/proto + - go get github.com/Knetic/govaluate + - go get github.com/hsluoyz/casbin - go get -u honnef.co/go/tools/cmd/gosimple - go get -u github.com/mdempsky/unconvert - go get -u github.com/gordonklaus/ineffassign diff --git a/plugins/authz/authz.go b/plugins/authz/authz.go new file mode 100644 index 00000000..709a613a --- /dev/null +++ b/plugins/authz/authz.go @@ -0,0 +1,86 @@ +// Copyright 2014 beego Author. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package authz provides handlers to enable ACL, RBAC, ABAC authorization support. +// Simple Usage: +// import( +// "github.com/astaxie/beego" +// "github.com/astaxie/beego/plugins/authz" +// "github.com/hsluoyz/casbin" +// ) +// +// func main(){ +// // mediate the access for every request +// beego.InsertFilter("*", beego.BeforeRouter, authz.NewAuthorizer(casbin.NewEnforcer("authz_model.conf", "authz_policy.csv"))) +// beego.Run() +// } +// +// +// Advanced Usage: +// +// func main(){ +// e := casbin.NewEnforcer("authz_model.conf", "") +// e.AddRoleForUser("alice", "admin") +// e.AddPolicy(...) +// +// beego.InsertFilter("*", beego.BeforeRouter, authz.NewAuthorizer(e)) +// beego.Run() +// } +package authz + +import ( + "github.com/astaxie/beego" + "github.com/astaxie/beego/context" + "github.com/hsluoyz/casbin" + "net/http" +) + +// NewAuthorizer returns the authorizer. +// Use a casbin enforcer as input +func NewAuthorizer(e *casbin.Enforcer) beego.FilterFunc { + return func(ctx *context.Context) { + a := &BasicAuthorizer{enforcer: e} + + if !a.CheckPermission(ctx.Request) { + a.RequirePermission(ctx.ResponseWriter) + } + } +} + +// BasicAuthorizer stores the casbin handler +type BasicAuthorizer struct { + enforcer *casbin.Enforcer +} + +// GetUserName gets the user name from the request. +// Currently, only HTTP basic authentication is supported +func (a *BasicAuthorizer) GetUserName(r *http.Request) string { + username, _, _ := r.BasicAuth() + return username +} + +// CheckPermission checks the user/method/path combination from the request. +// Returns true (permission granted) or false (permission forbidden) +func (a *BasicAuthorizer) CheckPermission(r *http.Request) bool { + user := a.GetUserName(r) + method := r.Method + path := r.URL.Path + return a.enforcer.Enforce(user, path, method) +} + +// RequirePermission returns the 403 Forbidden to the client +func (a *BasicAuthorizer) RequirePermission(w http.ResponseWriter) { + w.WriteHeader(403) + w.Write([]byte("403 Forbidden\n")) +} diff --git a/plugins/authz/authz_model.conf b/plugins/authz/authz_model.conf new file mode 100644 index 00000000..d1b3dbd7 --- /dev/null +++ b/plugins/authz/authz_model.conf @@ -0,0 +1,14 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*") \ No newline at end of file diff --git a/plugins/authz/authz_policy.csv b/plugins/authz/authz_policy.csv new file mode 100644 index 00000000..c062dd3e --- /dev/null +++ b/plugins/authz/authz_policy.csv @@ -0,0 +1,7 @@ +p, alice, /dataset1/*, GET +p, alice, /dataset1/resource1, POST +p, bob, /dataset2/resource1, * +p, bob, /dataset2/resource2, GET +p, bob, /dataset2/folder1/*, POST +p, dataset1_admin, /dataset1/*, * +g, cathy, dataset1_admin \ No newline at end of file diff --git a/plugins/authz/authz_test.go b/plugins/authz/authz_test.go new file mode 100644 index 00000000..4003582c --- /dev/null +++ b/plugins/authz/authz_test.go @@ -0,0 +1,107 @@ +// Copyright 2014 beego Author. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package authz + +import ( + "github.com/astaxie/beego" + "github.com/astaxie/beego/context" + "github.com/astaxie/beego/plugins/auth" + "github.com/hsluoyz/casbin" + "net/http" + "net/http/httptest" + "testing" +) + +func testRequest(t *testing.T, handler *beego.ControllerRegister, user string, path string, method string, code int) { + r, _ := http.NewRequest(method, path, nil) + r.SetBasicAuth(user, "123") + w := httptest.NewRecorder() + handler.ServeHTTP(w, r) + + if w.Code != code { + t.Errorf("%s, %s, %s: %d, supposed to be %d", user, path, method, w.Code, code) + } +} + +func TestBasic(t *testing.T) { + handler := beego.NewControllerRegister() + + handler.InsertFilter("*", beego.BeforeRouter, auth.Basic("alice", "123")) + handler.InsertFilter("*", beego.BeforeRouter, NewAuthorizer(casbin.NewEnforcer("authz_model.conf", "authz_policy.csv"))) + + handler.Any("*", func(ctx *context.Context) { + ctx.Output.SetStatus(200) + }) + + testRequest(t, handler, "alice", "/dataset1/resource1", "GET", 200) + testRequest(t, handler, "alice", "/dataset1/resource1", "POST", 200) + testRequest(t, handler, "alice", "/dataset1/resource2", "GET", 200) + testRequest(t, handler, "alice", "/dataset1/resource2", "POST", 403) +} + +func TestPathWildcard(t *testing.T) { + handler := beego.NewControllerRegister() + + handler.InsertFilter("*", beego.BeforeRouter, auth.Basic("bob", "123")) + handler.InsertFilter("*", beego.BeforeRouter, NewAuthorizer(casbin.NewEnforcer("authz_model.conf", "authz_policy.csv"))) + + handler.Any("*", func(ctx *context.Context) { + ctx.Output.SetStatus(200) + }) + + testRequest(t, handler, "bob", "/dataset2/resource1", "GET", 200) + testRequest(t, handler, "bob", "/dataset2/resource1", "POST", 200) + testRequest(t, handler, "bob", "/dataset2/resource1", "DELETE", 200) + testRequest(t, handler, "bob", "/dataset2/resource2", "GET", 200) + testRequest(t, handler, "bob", "/dataset2/resource2", "POST", 403) + testRequest(t, handler, "bob", "/dataset2/resource2", "DELETE", 403) + + testRequest(t, handler, "bob", "/dataset2/folder1/item1", "GET", 403) + testRequest(t, handler, "bob", "/dataset2/folder1/item1", "POST", 200) + testRequest(t, handler, "bob", "/dataset2/folder1/item1", "DELETE", 403) + testRequest(t, handler, "bob", "/dataset2/folder1/item2", "GET", 403) + testRequest(t, handler, "bob", "/dataset2/folder1/item2", "POST", 200) + testRequest(t, handler, "bob", "/dataset2/folder1/item2", "DELETE", 403) +} + +func TestRBAC(t *testing.T) { + handler := beego.NewControllerRegister() + + handler.InsertFilter("*", beego.BeforeRouter, auth.Basic("cathy", "123")) + e := casbin.NewEnforcer("authz_model.conf", "authz_policy.csv") + handler.InsertFilter("*", beego.BeforeRouter, NewAuthorizer(e)) + + handler.Any("*", func(ctx *context.Context) { + ctx.Output.SetStatus(200) + }) + + // cathy can access all /dataset1/* resources via all methods because it has the dataset1_admin role. + testRequest(t, handler, "cathy", "/dataset1/item", "GET", 200) + testRequest(t, handler, "cathy", "/dataset1/item", "POST", 200) + testRequest(t, handler, "cathy", "/dataset1/item", "DELETE", 200) + testRequest(t, handler, "cathy", "/dataset2/item", "GET", 403) + testRequest(t, handler, "cathy", "/dataset2/item", "POST", 403) + testRequest(t, handler, "cathy", "/dataset2/item", "DELETE", 403) + + // delete all roles on user cathy, so cathy cannot access any resources now. + e.DeleteRolesForUser("cathy") + + testRequest(t, handler, "cathy", "/dataset1/item", "GET", 403) + testRequest(t, handler, "cathy", "/dataset1/item", "POST", 403) + testRequest(t, handler, "cathy", "/dataset1/item", "DELETE", 403) + testRequest(t, handler, "cathy", "/dataset2/item", "GET", 403) + testRequest(t, handler, "cathy", "/dataset2/item", "POST", 403) + testRequest(t, handler, "cathy", "/dataset2/item", "DELETE", 403) +} From cb4f252a06455ac1a4f9e15d7d83e3c33cd188fb Mon Sep 17 00:00:00 2001 From: Eyal Post Date: Thu, 11 May 2017 17:58:25 +0300 Subject: [PATCH 13/32] defValue -> defaultValue --- context/param/conv.go | 5 +---- context/param/methodparams.go | 12 ++++++------ context/param/options.go | 6 +++--- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/context/param/conv.go b/context/param/conv.go index e961b4ad..596d2b1e 100644 --- a/context/param/conv.go +++ b/context/param/conv.go @@ -24,7 +24,7 @@ func convertParam(param *MethodParam, paramType reflect.Type, ctx *beecontext.Co if param.required { ctx.Abort(400, fmt.Sprintf("Missing parameter %s", param.name)) } else { - paramValue = param.defValue + paramValue = param.defaultValue } } @@ -43,9 +43,6 @@ func getParamValue(param *MethodParam, ctx *beecontext.Context) string { return string(ctx.Input.RequestBody) case header: return ctx.Input.Header(param.name) - // if strValue == "" && strings.Contains(param.name, "_") { //magically handle X-Headers? - // strValue = ctx.Input.Header(strings.Replace(param.name, "_", "-", -1)) - // } case path: return ctx.Input.Query(":" + param.name) default: diff --git a/context/param/methodparams.go b/context/param/methodparams.go index 28772504..fe4ab421 100644 --- a/context/param/methodparams.go +++ b/context/param/methodparams.go @@ -7,10 +7,10 @@ import ( //MethodParam keeps param information to be auto passed to controller methods type MethodParam struct { - name string - location paramLocation - required bool - defValue string + name string + location paramLocation + required bool + defaultValue string } type paramLocation byte @@ -57,8 +57,8 @@ func (mp *MethodParam) String() string { case header: options = append(options, "param.InHeader") } - if mp.defValue != "" { - options = append(options, fmt.Sprintf(`param.Default("%s")`, mp.defValue)) + if mp.defaultValue != "" { + options = append(options, fmt.Sprintf(`param.Default("%s")`, mp.defaultValue)) } if len(options) > 0 { result += ", " diff --git a/context/param/options.go b/context/param/options.go index 846a59f6..32402194 100644 --- a/context/param/options.go +++ b/context/param/options.go @@ -28,10 +28,10 @@ var InBody MethodParamOption = func(p *MethodParam) { } // Default provides a default value for the http param -func Default(defValue interface{}) MethodParamOption { +func Default(defaultValue interface{}) MethodParamOption { return func(p *MethodParam) { - if defValue != nil { - p.defValue = fmt.Sprint(defValue) + if defaultValue != nil { + p.defaultValue = fmt.Sprint(defaultValue) } } } From 74dc3c750028aee134e648f988b2f270e4985645 Mon Sep 17 00:00:00 2001 From: Eyal Post Date: Thu, 11 May 2017 19:32:44 +0300 Subject: [PATCH 14/32] tests --- context/param/parsers_test.go | 45 +++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 context/param/parsers_test.go diff --git a/context/param/parsers_test.go b/context/param/parsers_test.go new file mode 100644 index 00000000..6d305018 --- /dev/null +++ b/context/param/parsers_test.go @@ -0,0 +1,45 @@ +package param + +import "testing" +import "reflect" +import "time" + +type testDefinition struct { + strValue string + expectedValue interface{} + expectedParser paramParser +} + +func Test_Parsers(t *testing.T) { + checkParser(testDefinition{"1", 1, intParser{}}, t) + + checkParser(testDefinition{"1.0", 1.0, floatParser{}}, t) + + checkParser(testDefinition{"1", "1", stringParser{}}, t) + + checkParser(testDefinition{"true", true, boolParser{}}, t) + + checkParser(testDefinition{"2017-05-30T13:54:53Z", time.Date(2017, 5, 30, 13, 54, 53, 0, time.UTC), timeParser{}}, t) + + checkParser(testDefinition{`{"X": 5}`, struct{ X int }{5}, jsonParser{}}, t) + + checkParser(testDefinition{`1,2`, []int{1, 2}, sliceParser(intParser{})}, t) +} + +func checkParser(def testDefinition, t *testing.T) { + toType := reflect.TypeOf(def.expectedValue) + parser := getParser(&MethodParam{}, toType) + + if reflect.TypeOf(parser) != reflect.TypeOf(def.expectedParser) { + t.Errorf("Invalid parser for value %v. Expected: %v, actual: %v", def.strValue, reflect.TypeOf(def.expectedParser).Name(), reflect.TypeOf(parser).Name()) + return + } + result, err := parser.parse(def.strValue, toType) + if err != nil { + t.Errorf("Parsing error for value %v. Expected result: %v, error: %v", def.strValue, def.expectedValue, err) + return + } + if !reflect.DeepEqual(result, def.expectedValue) { + t.Errorf("Parsing error for value %v. Expected result: %v, actual: %v", def.strValue, def.expectedValue, result) + } +} From b6a35a8944ee928dbbcc4fbfb3b1760a6c21d5c2 Mon Sep 17 00:00:00 2001 From: eyalpost Date: Fri, 12 May 2017 09:25:12 +0300 Subject: [PATCH 15/32] more tests --- context/param/parsers_test.go | 51 ++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/context/param/parsers_test.go b/context/param/parsers_test.go index 6d305018..cc97c735 100644 --- a/context/param/parsers_test.go +++ b/context/param/parsers_test.go @@ -11,24 +11,58 @@ type testDefinition struct { } func Test_Parsers(t *testing.T) { + + //ints checkParser(testDefinition{"1", 1, intParser{}}, t) + checkParser(testDefinition{"-1", int64(-1), intParser{}}, t) + checkParser(testDefinition{"1", uint64(1), intParser{}}, t) - checkParser(testDefinition{"1.0", 1.0, floatParser{}}, t) + //floats + checkParser(testDefinition{"1.0", float32(1.0), floatParser{}}, t) + checkParser(testDefinition{"-1.0", float64(-1.0), floatParser{}}, t) - checkParser(testDefinition{"1", "1", stringParser{}}, t) + //strings + checkParser(testDefinition{"AB", "AB", stringParser{}}, t) + checkParser(testDefinition{"AB", []byte{65, 66}, stringParser{}}, t) + //bools checkParser(testDefinition{"true", true, boolParser{}}, t) + checkParser(testDefinition{"0", false, boolParser{}}, t) + //timeParser checkParser(testDefinition{"2017-05-30T13:54:53Z", time.Date(2017, 5, 30, 13, 54, 53, 0, time.UTC), timeParser{}}, t) + checkParser(testDefinition{"2017-05-30", time.Date(2017, 5, 30, 0, 0, 0, 0, time.UTC), timeParser{}}, t) - checkParser(testDefinition{`{"X": 5}`, struct{ X int }{5}, jsonParser{}}, t) + //json + checkParser(testDefinition{`{"X": 5, "Y":"Z"}`, struct { + X int + Y string + }{5, "Z"}, jsonParser{}}, t) + //slice in query is parsed as comma delimited checkParser(testDefinition{`1,2`, []int{1, 2}, sliceParser(intParser{})}, t) + + //slice in body is parsed as json + checkParser(testDefinition{`["a","b"]`, []string{"a", "b"}, jsonParser{}}, t, MethodParam{location: body}) + + //pointers + var someInt = 1 + checkParser(testDefinition{`1`, &someInt, ptrParser(intParser{})}, t) + + var someStruct = struct{ X int }{5} + checkParser(testDefinition{`{"X": 5}`, &someStruct, jsonParser{}}, t) + } -func checkParser(def testDefinition, t *testing.T) { +func checkParser(def testDefinition, t *testing.T, methodParam ...MethodParam) { toType := reflect.TypeOf(def.expectedValue) - parser := getParser(&MethodParam{}, toType) + var mp MethodParam + if len(methodParam) == 0 { + mp = MethodParam{} + } else { + mp = methodParam[0] + } + parser := getParser(&mp, toType) if reflect.TypeOf(parser) != reflect.TypeOf(def.expectedParser) { t.Errorf("Invalid parser for value %v. Expected: %v, actual: %v", def.strValue, reflect.TypeOf(def.expectedParser).Name(), reflect.TypeOf(parser).Name()) @@ -39,7 +73,12 @@ func checkParser(def testDefinition, t *testing.T) { t.Errorf("Parsing error for value %v. Expected result: %v, error: %v", def.strValue, def.expectedValue, err) return } - if !reflect.DeepEqual(result, def.expectedValue) { + convResult, err := safeConvert(reflect.ValueOf(result), toType) + if err != nil { + t.Errorf("Convertion error for %v. from value: %v, toType: %v, error: %v", def.strValue, result, toType, err) + return + } + if !reflect.DeepEqual(convResult.Interface(), def.expectedValue) { t.Errorf("Parsing error for value %v. Expected result: %v, actual: %v", def.strValue, def.expectedValue, result) } } From 0ac2e4716248cd0ec1e77e704f394fe87442a674 Mon Sep 17 00:00:00 2001 From: eyalpost Date: Fri, 12 May 2017 09:28:46 +0300 Subject: [PATCH 16/32] location=>paramType --- context/param/conv.go | 2 +- context/param/methodparams.go | 8 ++++---- context/param/options.go | 6 +++--- context/param/parsers.go | 2 +- context/param/parsers_test.go | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/context/param/conv.go b/context/param/conv.go index 596d2b1e..c200e008 100644 --- a/context/param/conv.go +++ b/context/param/conv.go @@ -38,7 +38,7 @@ func convertParam(param *MethodParam, paramType reflect.Type, ctx *beecontext.Co } func getParamValue(param *MethodParam, ctx *beecontext.Context) string { - switch param.location { + switch param.in { case body: return string(ctx.Input.RequestBody) case header: diff --git a/context/param/methodparams.go b/context/param/methodparams.go index fe4ab421..cd6708a2 100644 --- a/context/param/methodparams.go +++ b/context/param/methodparams.go @@ -8,15 +8,15 @@ import ( //MethodParam keeps param information to be auto passed to controller methods type MethodParam struct { name string - location paramLocation + in paramType required bool defaultValue string } -type paramLocation byte +type paramType byte const ( - param paramLocation = iota + param paramType = iota path body header @@ -49,7 +49,7 @@ func (mp *MethodParam) String() string { if mp.required { options = append(options, "param.IsRequired") } - switch mp.location { + switch mp.in { case path: options = append(options, "param.InPath") case body: diff --git a/context/param/options.go b/context/param/options.go index 32402194..58bdc3d0 100644 --- a/context/param/options.go +++ b/context/param/options.go @@ -14,17 +14,17 @@ var IsRequired MethodParamOption = func(p *MethodParam) { // InHeader indicates that this param is passed via an http header var InHeader MethodParamOption = func(p *MethodParam) { - p.location = header + p.in = header } // InPath indicates that this param is part of the URL path var InPath MethodParamOption = func(p *MethodParam) { - p.location = path + p.in = path } // InBody indicates that this param is passed as an http request body var InBody MethodParamOption = func(p *MethodParam) { - p.location = body + p.in = body } // Default provides a default value for the http param diff --git a/context/param/parsers.go b/context/param/parsers.go index 2b48e878..421aecf0 100644 --- a/context/param/parsers.go +++ b/context/param/parsers.go @@ -21,7 +21,7 @@ func getParser(param *MethodParam, t reflect.Type) paramParser { if t.Elem().Kind() == reflect.Uint8 { //treat []byte as string return stringParser{} } - if param.location == body { + if param.in == body { return jsonParser{} } elemParser := getParser(param, t.Elem()) diff --git a/context/param/parsers_test.go b/context/param/parsers_test.go index cc97c735..b946ba08 100644 --- a/context/param/parsers_test.go +++ b/context/param/parsers_test.go @@ -43,7 +43,7 @@ func Test_Parsers(t *testing.T) { checkParser(testDefinition{`1,2`, []int{1, 2}, sliceParser(intParser{})}, t) //slice in body is parsed as json - checkParser(testDefinition{`["a","b"]`, []string{"a", "b"}, jsonParser{}}, t, MethodParam{location: body}) + checkParser(testDefinition{`["a","b"]`, []string{"a", "b"}, jsonParser{}}, t, MethodParam{in: body}) //pointers var someInt = 1 From 1004678005135f5d7a6eb940613724ec56a87741 Mon Sep 17 00:00:00 2001 From: eyalpost Date: Fri, 12 May 2017 09:57:56 +0300 Subject: [PATCH 17/32] popular status codes --- context/response/renderer.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/context/response/renderer.go b/context/response/renderer.go index 2a4d2797..38d0cdf6 100644 --- a/context/response/renderer.go +++ b/context/response/renderer.go @@ -3,9 +3,16 @@ package response import ( "strconv" + "net/http" + beecontext "github.com/astaxie/beego/context" ) +const ( + NotFound StatusCode = http.StatusNotFound + BadRequest StatusCode = http.StatusBadRequest +) + // Renderer defines an http response renderer type Renderer interface { Render(ctx *beecontext.Context) From 589f3755f07cbdddeb92f6451bbb60b343eae7c9 Mon Sep 17 00:00:00 2001 From: sunxinle Date: Fri, 12 May 2017 18:11:42 +0800 Subject: [PATCH 18/32] =?UTF-8?q?=E5=85=81=E8=AE=B8o.Raw(sql).QueryRows(&c?= =?UTF-8?q?ontainer)=20=E4=BC=A0=E5=85=A5=E7=9A=84container=E5=8C=85?= =?UTF-8?q?=E5=90=AB=E7=BB=93=E6=9E=84=E7=9A=84=E5=B5=8C=E5=A5=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- orm/orm_raw.go | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/orm/orm_raw.go b/orm/orm_raw.go index 1e86212a..c8e741ea 100644 --- a/orm/orm_raw.go +++ b/orm/orm_raw.go @@ -493,19 +493,33 @@ func (o *rawSet) QueryRows(containers ...interface{}) (int64, error) { } } } else { - for i := 0; i < ind.NumField(); i++ { - f := ind.Field(i) - fe := ind.Type().Field(i) - _, tags := parseStructTag(fe.Tag.Get(defaultStructTagName)) - var col string - if col = tags["column"]; col == "" { - col = snakeString(fe.Name) - } - if v, ok := columnsMp[col]; ok { - value := reflect.ValueOf(v).Elem().Interface() - o.setFieldValue(f, value) + // define recursive function + var recursiveSetField func(rv reflect.Value) + recursiveSetField = func(rv reflect.Value) { + for i := 0; i < rv.NumField(); i++ { + f := rv.Field(i) + fe := rv.Type().Field(i) + + // check if the field is a Struct + // recursive the Struct type + if fe.Type.Kind() == reflect.Struct { + recursiveSetField(f) + } + + _, tags := parseStructTag(fe.Tag.Get(defaultStructTagName)) + var col string + if col = tags["column"]; col == "" { + col = snakeString(fe.Name) + } + if v, ok := columnsMp[col]; ok { + value := reflect.ValueOf(v).Elem().Interface() + o.setFieldValue(f, value) + } } } + + // init call the recursive function + recursiveSetField(ind) } if eTyps[0].Kind() == reflect.Ptr { From 23250901018690f888de783a117c6858b9d3ca46 Mon Sep 17 00:00:00 2001 From: alexsunxl Date: Sun, 14 May 2017 12:03:34 +0800 Subject: [PATCH 19/32] add test case that used nested struct test QueryRows --- orm/orm_test.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/orm/orm_test.go b/orm/orm_test.go index c5bfa8b9..f1f2d85e 100644 --- a/orm/orm_test.go +++ b/orm/orm_test.go @@ -1661,6 +1661,13 @@ func TestRawQueryRow(t *testing.T) { throwFail(t, AssertIs(pid, nil)) } +// user_profile table +type userProfile struct { + User + Age int + Money float64 +} + func TestQueryRows(t *testing.T) { Q := dDbBaser.TableQuote() @@ -1731,6 +1738,19 @@ func TestQueryRows(t *testing.T) { throwFailNow(t, AssertIs(usernames[1], "astaxie")) throwFailNow(t, AssertIs(ids[2], 4)) throwFailNow(t, AssertIs(usernames[2], "nobody")) + + //test query rows by nested struct + var l []userProfile + query = fmt.Sprintf("SELECT * FROM %suser_profile%s LEFT JOIN %suser%s ON %suser_profile%s.%sid%s = %suser%s.%sid%s", Q, Q, Q, Q, Q, Q, Q, Q, Q, Q, Q, Q) + num, err = dORM.Raw(query).QueryRows(&l) + throwFailNow(t, err) + throwFailNow(t, AssertIs(num, 2)) + throwFailNow(t, AssertIs(len(l), 2)) + throwFailNow(t, AssertIs(l[0].UserName, "slene")) + throwFailNow(t, AssertIs(l[0].Age, 28)) + throwFailNow(t, AssertIs(l[1].UserName, "astaxie")) + throwFailNow(t, AssertIs(l[1].Age, 30)) + } func TestRawValues(t *testing.T) { From e1c90bfc09108111a8290ecb7535c9ec60f34b59 Mon Sep 17 00:00:00 2001 From: Robert Wikman Date: Tue, 16 May 2017 00:27:57 +0200 Subject: [PATCH 20/32] Table not found spelling fixes --- orm/orm.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/orm/orm.go b/orm/orm.go index 5db79386..fcf82590 100644 --- a/orm/orm.go +++ b/orm/orm.go @@ -107,7 +107,7 @@ func (o *orm) getMiInd(md interface{}, needPtr bool) (mi *modelInfo, ind reflect if mi, ok := modelCache.getByFullName(name); ok { return mi, ind } - panic(fmt.Errorf(" table: `%s` not found, maybe not RegisterModel", name)) + panic(fmt.Errorf(" table: `%s` not found, make sure it was registered with `RegisterModel()`", name)) } // get field info from model info by given field name From b5c6eb54d28efd3a655fa676f11542305106a7c6 Mon Sep 17 00:00:00 2001 From: Robert Wikman Date: Tue, 16 May 2017 00:58:20 +0200 Subject: [PATCH 21/32] Missing PK error spelling fix --- orm/models_boot.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/orm/models_boot.go b/orm/models_boot.go index 85d0917f..5327f754 100644 --- a/orm/models_boot.go +++ b/orm/models_boot.go @@ -75,7 +75,7 @@ func registerModel(PrefixOrSuffix string, model interface{}, isPrefix bool) { } if mi.fields.pk == nil { - fmt.Printf(" `%s` need a primary key field, default use 'id' if not set\n", name) + fmt.Printf(" `%s` needs a primary key field, default is to use 'id' if not set\n", name) os.Exit(2) } From 69f0b947452dc1723e0f693d64fc5ee712b8d1d0 Mon Sep 17 00:00:00 2001 From: astaxie Date: Tue, 16 May 2017 22:21:43 +0800 Subject: [PATCH 22/32] fix gosimple --- templatefunc.go | 10 ++++----- validation/validation_test.go | 8 ++++---- validation/validators.go | 38 +++++++++++++++++------------------ 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/templatefunc.go b/templatefunc.go index a6f9c961..2591c88e 100644 --- a/templatefunc.go +++ b/templatefunc.go @@ -53,21 +53,21 @@ func Substr(s string, start, length int) string { // HTML2str returns escaping text convert from html. func HTML2str(html string) string { - re, _ := regexp.Compile("\\<[\\S\\s]+?\\>") + re, _ := regexp.Compile(`\<[\S\s]+?\>`) html = re.ReplaceAllStringFunc(html, strings.ToLower) //remove STYLE - re, _ = regexp.Compile("\\") + re, _ = regexp.Compile(`\`) html = re.ReplaceAllString(html, "") //remove SCRIPT - re, _ = regexp.Compile("\\") + re, _ = regexp.Compile(`\`) html = re.ReplaceAllString(html, "") - re, _ = regexp.Compile("\\<[\\S\\s]+?\\>") + re, _ = regexp.Compile(`\<[\S\s]+?\>`) html = re.ReplaceAllString(html, "\n") - re, _ = regexp.Compile("\\s{2,}") + re, _ = regexp.Compile(`\s{2,}`) html = re.ReplaceAllString(html, "\n") return strings.TrimSpace(html) diff --git a/validation/validation_test.go b/validation/validation_test.go index 83e881bf..cb7a297e 100644 --- a/validation/validation_test.go +++ b/validation/validation_test.go @@ -175,10 +175,10 @@ func TestAlphaNumeric(t *testing.T) { func TestMatch(t *testing.T) { valid := Validation{} - if valid.Match("suchuangji@gmail", regexp.MustCompile("^\\w+@\\w+\\.\\w+$"), "match").Ok { + if valid.Match("suchuangji@gmail", regexp.MustCompile(`^\w+@\w+\.\w+$`), "match").Ok { t.Error("\"suchuangji@gmail\" match \"^\\w+@\\w+\\.\\w+$\" should be false") } - if !valid.Match("suchuangji@gmail.com", regexp.MustCompile("^\\w+@\\w+\\.\\w+$"), "match").Ok { + if !valid.Match("suchuangji@gmail.com", regexp.MustCompile(`^\w+@\w+\.\w+$`), "match").Ok { t.Error("\"suchuangji@gmail\" match \"^\\w+@\\w+\\.\\w+$\" should be true") } } @@ -186,10 +186,10 @@ func TestMatch(t *testing.T) { func TestNoMatch(t *testing.T) { valid := Validation{} - if valid.NoMatch("123@gmail", regexp.MustCompile("[^\\w\\d]"), "nomatch").Ok { + if valid.NoMatch("123@gmail", regexp.MustCompile(`[^\w\d]`), "nomatch").Ok { t.Error("\"123@gmail\" not match \"[^\\w\\d]\" should be false") } - if !valid.NoMatch("123gmail", regexp.MustCompile("[^\\w\\d]"), "match").Ok { + if !valid.NoMatch("123gmail", regexp.MustCompile(`[^\w\d]`), "match").Ok { t.Error("\"123@gmail\" not match \"[^\\w\\d@]\" should be true") } } diff --git a/validation/validators.go b/validation/validators.go index 01aed443..2b5ae38b 100644 --- a/validation/validators.go +++ b/validation/validators.go @@ -145,7 +145,7 @@ func (r Required) IsSatisfied(obj interface{}) bool { // DefaultMessage return the default error message func (r Required) DefaultMessage() string { - return fmt.Sprint(MessageTmpls["Required"]) + return MessageTmpls["Required"] } // GetKey return the r.Key @@ -364,7 +364,7 @@ func (a Alpha) IsSatisfied(obj interface{}) bool { // DefaultMessage return the default Length error message func (a Alpha) DefaultMessage() string { - return fmt.Sprint(MessageTmpls["Alpha"]) + return MessageTmpls["Alpha"] } // GetKey return the m.Key @@ -397,7 +397,7 @@ func (n Numeric) IsSatisfied(obj interface{}) bool { // DefaultMessage return the default Length error message func (n Numeric) DefaultMessage() string { - return fmt.Sprint(MessageTmpls["Numeric"]) + return MessageTmpls["Numeric"] } // GetKey return the n.Key @@ -430,7 +430,7 @@ func (a AlphaNumeric) IsSatisfied(obj interface{}) bool { // DefaultMessage return the default Length error message func (a AlphaNumeric) DefaultMessage() string { - return fmt.Sprint(MessageTmpls["AlphaNumeric"]) + return MessageTmpls["AlphaNumeric"] } // GetKey return the a.Key @@ -495,7 +495,7 @@ func (n NoMatch) GetLimitValue() interface{} { return n.Regexp.String() } -var alphaDashPattern = regexp.MustCompile("[^\\d\\w-_]") +var alphaDashPattern = regexp.MustCompile(`[^\d\w-_]`) // AlphaDash check not Alpha type AlphaDash struct { @@ -505,7 +505,7 @@ type AlphaDash struct { // DefaultMessage return the default AlphaDash error message func (a AlphaDash) DefaultMessage() string { - return fmt.Sprint(MessageTmpls["AlphaDash"]) + return MessageTmpls["AlphaDash"] } // GetKey return the n.Key @@ -518,7 +518,7 @@ func (a AlphaDash) GetLimitValue() interface{} { return nil } -var emailPattern = regexp.MustCompile("^[\\w!#$%&'*+/=?^_`{|}~-]+(?:\\.[\\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\\w](?:[\\w-]*[\\w])?\\.)+[a-zA-Z0-9](?:[\\w-]*[\\w])?$") +var emailPattern = regexp.MustCompile(`^[\w!#$%&'*+/=?^_` + "`" + `{|}~-]+(?:\.[\w!#$%&'*+/=?^_` + "`" + `{|}~-]+)*@(?:[\w](?:[\w-]*[\w])?\.)+[a-zA-Z0-9](?:[\w-]*[\w])?$`) // Email check struct type Email struct { @@ -528,7 +528,7 @@ type Email struct { // DefaultMessage return the default Email error message func (e Email) DefaultMessage() string { - return fmt.Sprint(MessageTmpls["Email"]) + return MessageTmpls["Email"] } // GetKey return the n.Key @@ -541,7 +541,7 @@ func (e Email) GetLimitValue() interface{} { return nil } -var ipPattern = regexp.MustCompile("^((2[0-4]\\d|25[0-5]|[01]?\\d\\d?)\\.){3}(2[0-4]\\d|25[0-5]|[01]?\\d\\d?)$") +var ipPattern = regexp.MustCompile(`^((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)$`) // IP check struct type IP struct { @@ -551,7 +551,7 @@ type IP struct { // DefaultMessage return the default IP error message func (i IP) DefaultMessage() string { - return fmt.Sprint(MessageTmpls["IP"]) + return MessageTmpls["IP"] } // GetKey return the i.Key @@ -564,7 +564,7 @@ func (i IP) GetLimitValue() interface{} { return nil } -var base64Pattern = regexp.MustCompile("^(?:[A-Za-z0-99+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$") +var base64Pattern = regexp.MustCompile(`^(?:[A-Za-z0-99+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$`) // Base64 check struct type Base64 struct { @@ -574,7 +574,7 @@ type Base64 struct { // DefaultMessage return the default Base64 error message func (b Base64) DefaultMessage() string { - return fmt.Sprint(MessageTmpls["Base64"]) + return MessageTmpls["Base64"] } // GetKey return the b.Key @@ -588,7 +588,7 @@ func (b Base64) GetLimitValue() interface{} { } // just for chinese mobile phone number -var mobilePattern = regexp.MustCompile("^((\\+86)|(86))?(1(([35][0-9])|[8][0-9]|[7][06789]|[4][579]))\\d{8}$") +var mobilePattern = regexp.MustCompile(`^((\+86)|(86))?(1(([35][0-9])|[8][0-9]|[7][06789]|[4][579]))\d{8}$`) // Mobile check struct type Mobile struct { @@ -598,7 +598,7 @@ type Mobile struct { // DefaultMessage return the default Mobile error message func (m Mobile) DefaultMessage() string { - return fmt.Sprint(MessageTmpls["Mobile"]) + return MessageTmpls["Mobile"] } // GetKey return the m.Key @@ -612,7 +612,7 @@ func (m Mobile) GetLimitValue() interface{} { } // just for chinese telephone number -var telPattern = regexp.MustCompile("^(0\\d{2,3}(\\-)?)?\\d{7,8}$") +var telPattern = regexp.MustCompile(`^(0\d{2,3}(\-)?)?\d{7,8}$`) // Tel check telephone struct type Tel struct { @@ -622,7 +622,7 @@ type Tel struct { // DefaultMessage return the default Tel error message func (t Tel) DefaultMessage() string { - return fmt.Sprint(MessageTmpls["Tel"]) + return MessageTmpls["Tel"] } // GetKey return the t.Key @@ -649,7 +649,7 @@ func (p Phone) IsSatisfied(obj interface{}) bool { // DefaultMessage return the default Phone error message func (p Phone) DefaultMessage() string { - return fmt.Sprint(MessageTmpls["Phone"]) + return MessageTmpls["Phone"] } // GetKey return the p.Key @@ -663,7 +663,7 @@ func (p Phone) GetLimitValue() interface{} { } // just for chinese zipcode -var zipCodePattern = regexp.MustCompile("^[1-9]\\d{5}$") +var zipCodePattern = regexp.MustCompile(`^[1-9]\d{5}$`) // ZipCode check the zip struct type ZipCode struct { @@ -673,7 +673,7 @@ type ZipCode struct { // DefaultMessage return the default Zip error message func (z ZipCode) DefaultMessage() string { - return fmt.Sprint(MessageTmpls["ZipCode"]) + return MessageTmpls["ZipCode"] } // GetKey return the z.Key From 828cbbdf5d265d521bcc12300d9c750e7df505cc Mon Sep 17 00:00:00 2001 From: Eyal Post Date: Wed, 17 May 2017 20:38:59 +0300 Subject: [PATCH 23/32] Refactor a bit to consolidate packages --- context/context.go | 16 +++++++++++ context/output.go | 13 +++++++++ context/renderer.go | 12 ++++++++ context/response/renderer.go | 46 ------------------------------ context/response/responses.go | 53 ----------------------------------- httpResponse/response.go | 52 ++++++++++++++++++++++++++++++++++ router.go | 3 +- 7 files changed, 94 insertions(+), 101 deletions(-) create mode 100644 context/renderer.go delete mode 100644 context/response/renderer.go delete mode 100644 context/response/responses.go create mode 100644 httpResponse/response.go diff --git a/context/context.go b/context/context.go index 03286097..8b32062c 100644 --- a/context/context.go +++ b/context/context.go @@ -171,6 +171,22 @@ func (ctx *Context) CheckXSRFCookie() bool { return true } +// RenderMethodResult renders the return value of a controller method to the output +func (ctx *Context) RenderMethodResult(result interface{}) { + if result != nil { + renderer, ok := result.(Renderer) + if !ok { + err, ok := result.(error) + if ok { + renderer = errorRenderer(err) + } else { + renderer = jsonRenderer(result) + } + } + renderer.Render(ctx) + } +} + //Response is a wrapper for the http.ResponseWriter //started set to true if response was written to then don't execute other handler type Response struct { diff --git a/context/output.go b/context/output.go index 564ef96d..835552b0 100644 --- a/context/output.go +++ b/context/output.go @@ -168,6 +168,19 @@ func sanitizeValue(v string) string { return cookieValueSanitizer.Replace(v) } +func jsonRenderer(value interface{}) Renderer { + return rendererFunc(func(ctx *Context) { + ctx.Output.JSON(value, false, false) + }) +} + +func errorRenderer(err error) Renderer { + return rendererFunc(func(ctx *Context) { + ctx.Output.SetStatus(500) + ctx.WriteString(err.Error()) + }) +} + // JSON writes json to response body. // if coding is true, it converts utf-8 to \u0000 type. func (output *BeegoOutput) JSON(data interface{}, hasIndent bool, coding bool) error { diff --git a/context/renderer.go b/context/renderer.go new file mode 100644 index 00000000..36a7cb53 --- /dev/null +++ b/context/renderer.go @@ -0,0 +1,12 @@ +package context + +// Renderer defines an http response renderer +type Renderer interface { + Render(ctx *Context) +} + +type rendererFunc func(ctx *Context) + +func (f rendererFunc) Render(ctx *Context) { + f(ctx) +} diff --git a/context/response/renderer.go b/context/response/renderer.go deleted file mode 100644 index 38d0cdf6..00000000 --- a/context/response/renderer.go +++ /dev/null @@ -1,46 +0,0 @@ -package response - -import ( - "strconv" - - "net/http" - - beecontext "github.com/astaxie/beego/context" -) - -const ( - NotFound StatusCode = http.StatusNotFound - BadRequest StatusCode = http.StatusBadRequest -) - -// Renderer defines an http response renderer -type Renderer interface { - Render(ctx *beecontext.Context) -} - -type rendererFunc func(ctx *beecontext.Context) - -func (f rendererFunc) Render(ctx *beecontext.Context) { - f(ctx) -} - -// StatusCode sets the http response status code -type StatusCode int - -func (s StatusCode) Error() string { - return strconv.Itoa(int(s)) -} - -// Render sets the http status code -func (s StatusCode) Render(ctx *beecontext.Context) { - ctx.Output.SetStatus(int(s)) -} - -type statusCodeWithRender struct { - statusCode int - rendererFunc -} - -func (s statusCodeWithRender) Error() string { - return strconv.Itoa(s.statusCode) -} diff --git a/context/response/responses.go b/context/response/responses.go deleted file mode 100644 index 10d1ed3f..00000000 --- a/context/response/responses.go +++ /dev/null @@ -1,53 +0,0 @@ -package response - -import ( - beecontext "github.com/astaxie/beego/context" -) - -// JSON renders value to the response as JSON -func JSON(value interface{}, encoding ...bool) Renderer { - return rendererFunc(func(ctx *beecontext.Context) { - var ( - hasIndent = true - hasEncoding = false - ) - //TODO: need access to BConfig :( - // if BConfig.RunMode == PROD { - // hasIndent = false - // } - if len(encoding) > 0 && encoding[0] { - hasEncoding = true - } - ctx.Output.JSON(value, hasIndent, hasEncoding) - }) -} - -func errorRenderer(err error) Renderer { - return rendererFunc(func(ctx *beecontext.Context) { - ctx.Output.SetStatus(500) - ctx.WriteString(err.Error()) - }) -} - -// Redirect renders http 302 with a URL -func Redirect(localurl string) error { - return statusCodeWithRender{302, func(ctx *beecontext.Context) { - ctx.Redirect(302, localurl) - }} -} - -// RenderMethodResult renders the return value of a controller method to the output -func RenderMethodResult(result interface{}, ctx *beecontext.Context) { - if result != nil { - renderer, ok := result.(Renderer) - if !ok { - err, ok := result.(error) - if ok { - renderer = errorRenderer(err) - } else { - renderer = JSON(result) - } - } - renderer.Render(ctx) - } -} diff --git a/httpResponse/response.go b/httpResponse/response.go new file mode 100644 index 00000000..ca74b85c --- /dev/null +++ b/httpResponse/response.go @@ -0,0 +1,52 @@ +package httpResponse + +import ( + "strconv" + + "net/http" + + beecontext "github.com/astaxie/beego/context" +) + +const ( + //BadRequest indicates http error 400 + BadRequest StatusCode = http.StatusBadRequest + + //NotFound indicates http error 404 + NotFound StatusCode = http.StatusNotFound +) + +// Redirect renders http 302 with a URL +func Redirect(localurl string) error { + return statusCodeWithRender{302, func(ctx *beecontext.Context) { + ctx.Redirect(302, localurl) + }} +} + +// StatusCode sets the http response status code +type StatusCode int + +func (s StatusCode) Error() string { + return strconv.Itoa(int(s)) +} + +// Render sets the http status code +func (s StatusCode) Render(ctx *beecontext.Context) { + ctx.Output.SetStatus(int(s)) +} + +type statusCodeWithRender struct { + statusCode int + f func(ctx *beecontext.Context) +} + +//assert that statusCodeWithRender implements Renderer interface +var _r beecontext.Renderer = (*statusCodeWithRender)(nil) + +func (s statusCodeWithRender) Error() string { + return strconv.Itoa(s.statusCode) +} + +func (s statusCodeWithRender) Render(ctx *beecontext.Context) { + s.f(ctx) +} diff --git a/router.go b/router.go index 33096039..72476ae8 100644 --- a/router.go +++ b/router.go @@ -28,7 +28,6 @@ import ( beecontext "github.com/astaxie/beego/context" "github.com/astaxie/beego/context/param" - "github.com/astaxie/beego/context/response" "github.com/astaxie/beego/logs" "github.com/astaxie/beego/toolbox" "github.com/astaxie/beego/utils" @@ -905,7 +904,7 @@ func (p *ControllerRegister) handleParamResponse(context *beecontext.Context, ex result := results[i] if result.Kind() != reflect.Interface || !result.IsNil() { resultValue := result.Interface() - response.RenderMethodResult(resultValue, context) + context.RenderMethodResult(resultValue) } } if !context.ResponseWriter.Started && context.Output.Status == 0 { From ee1d8bc30ea2d89ecdb70402c7b1213ac8a4e5f1 Mon Sep 17 00:00:00 2001 From: Eyal Post Date: Wed, 17 May 2017 20:50:41 +0300 Subject: [PATCH 24/32] fix gosimple --- parser.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parser.go b/parser.go index 037d5376..a9cfd894 100644 --- a/parser.go +++ b/parser.go @@ -235,7 +235,7 @@ func getparams(str string) []string { var start bool var r []string var quoted int8 - for _, c := range []rune(str) { + for _, c := range str { if unicode.IsSpace(c) && quoted == 0 { if !start { continue From e32a18203b95391f627539c440834d65f704492a Mon Sep 17 00:00:00 2001 From: Eyal Post Date: Wed, 17 May 2017 21:27:32 +0300 Subject: [PATCH 25/32] fix gosimple --- context/output.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/context/output.go b/context/output.go index 835552b0..cf9e7a7e 100644 --- a/context/output.go +++ b/context/output.go @@ -343,9 +343,8 @@ func (output *BeegoOutput) IsServerError() bool { } func stringsToJSON(str string) string { - rs := []rune(str) var jsons bytes.Buffer - for _, r := range rs { + for _, r := range str { rint := int(r) if rint < 128 { jsons.WriteRune(r) From 3e51823c0faba9dcf541ec72c90664a30fedceb6 Mon Sep 17 00:00:00 2001 From: Eyal Post Date: Thu, 18 May 2017 09:05:49 +0300 Subject: [PATCH 26/32] move response --- {httpResponse => context/httpResponse}/response.go | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {httpResponse => context/httpResponse}/response.go (100%) diff --git a/httpResponse/response.go b/context/httpResponse/response.go similarity index 100% rename from httpResponse/response.go rename to context/httpResponse/response.go From 2513bcf584f655379bd5195dcaba83649aab9e8f Mon Sep 17 00:00:00 2001 From: Eyal Post Date: Thu, 18 May 2017 10:32:51 +0300 Subject: [PATCH 27/32] remove Redirect to avoid confusion --- context/httpResponse/response.go | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/context/httpResponse/response.go b/context/httpResponse/response.go index ca74b85c..0b013719 100644 --- a/context/httpResponse/response.go +++ b/context/httpResponse/response.go @@ -16,13 +16,6 @@ const ( NotFound StatusCode = http.StatusNotFound ) -// Redirect renders http 302 with a URL -func Redirect(localurl string) error { - return statusCodeWithRender{302, func(ctx *beecontext.Context) { - ctx.Redirect(302, localurl) - }} -} - // StatusCode sets the http response status code type StatusCode int @@ -34,19 +27,3 @@ func (s StatusCode) Error() string { func (s StatusCode) Render(ctx *beecontext.Context) { ctx.Output.SetStatus(int(s)) } - -type statusCodeWithRender struct { - statusCode int - f func(ctx *beecontext.Context) -} - -//assert that statusCodeWithRender implements Renderer interface -var _r beecontext.Renderer = (*statusCodeWithRender)(nil) - -func (s statusCodeWithRender) Error() string { - return strconv.Itoa(s.statusCode) -} - -func (s statusCodeWithRender) Render(ctx *beecontext.Context) { - s.f(ctx) -} From 11b4bf8aaa7ba08f7ca0aba61ebf1cee0c7d83a6 Mon Sep 17 00:00:00 2001 From: Eyal Post Date: Thu, 18 May 2017 10:38:12 +0300 Subject: [PATCH 28/32] move to context --- context/{httpResponse => }/response.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) rename context/{httpResponse => }/response.go (77%) diff --git a/context/httpResponse/response.go b/context/response.go similarity index 77% rename from context/httpResponse/response.go rename to context/response.go index 0b013719..9c3c715a 100644 --- a/context/httpResponse/response.go +++ b/context/response.go @@ -1,11 +1,9 @@ -package httpResponse +package context import ( "strconv" "net/http" - - beecontext "github.com/astaxie/beego/context" ) const ( @@ -24,6 +22,6 @@ func (s StatusCode) Error() string { } // Render sets the http status code -func (s StatusCode) Render(ctx *beecontext.Context) { +func (s StatusCode) Render(ctx *Context) { ctx.Output.SetStatus(int(s)) } From 248beab557b9090d28dc5dfd0806ee3a8113843d Mon Sep 17 00:00:00 2001 From: astaxie Date: Thu, 18 May 2017 22:55:10 +0800 Subject: [PATCH 29/32] v1.8.3 --- beego.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beego.go b/beego.go index 7a8db390..22079a20 100644 --- a/beego.go +++ b/beego.go @@ -23,7 +23,7 @@ import ( const ( // VERSION represent beego web framework version. - VERSION = "1.8.2" + VERSION = "1.8.3" // DEV is for develop DEV = "dev" From 47e351e11db86d120ccb3976d3c4388d07954139 Mon Sep 17 00:00:00 2001 From: Guohua Ouyang Date: Fri, 19 May 2017 09:22:27 +0800 Subject: [PATCH 30/32] Support timeformat "2006-01-02T15:04:05" Fixes #2649 Signed-off-by: Guohua Ouyang --- templatefunc.go | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/templatefunc.go b/templatefunc.go index 2591c88e..a104fd24 100644 --- a/templatefunc.go +++ b/templatefunc.go @@ -27,9 +27,10 @@ import ( ) const ( - formatTime = "15:04:05" - formatDate = "2006-01-02" - formatDateTime = "2006-01-02 15:04:05" + formatTime = "15:04:05" + formatDate = "2006-01-02" + formatDateTime = "2006-01-02 15:04:05" + formatDateTimeT = "2006-01-02T15:04:05" ) // Substr returns the substr from start to length. @@ -360,8 +361,13 @@ func parseFormToStruct(form url.Values, objT reflect.Type, objV reflect.Value) e value = value[:25] t, err = time.ParseInLocation(time.RFC3339, value, time.Local) } else if len(value) >= 19 { - value = value[:19] - t, err = time.ParseInLocation(formatDateTime, value, time.Local) + if strings.Contains(value, "T") { + value = value[:19] + t, err = time.ParseInLocation(formatDateTimeT, value, time.Local) + } else { + value = value[:19] + t, err = time.ParseInLocation(formatDateTime, value, time.Local) + } } else if len(value) >= 10 { if len(value) > 10 { value = value[:10] @@ -373,7 +379,6 @@ func parseFormToStruct(form url.Values, objT reflect.Type, objV reflect.Value) e } t, err = time.ParseInLocation(formatTime, value, time.Local) } - if err != nil { return err } From ce677202e557b330e2c06fde64922c609d66e822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=AE=AA=E0=AE=BE=E0=AE=B2=E0=AE=BE=E0=AE=9C=E0=AE=BF?= Date: Tue, 6 Dec 2016 19:25:31 +0530 Subject: [PATCH 31/32] issue no:#2261 fix for xsrf panic error --- error.go | 24 ++++++++++++++++++++++++ hooks.go | 2 ++ 2 files changed, 26 insertions(+) diff --git a/error.go b/error.go index ab626247..b913db39 100644 --- a/error.go +++ b/error.go @@ -252,6 +252,30 @@ func forbidden(rw http.ResponseWriter, r *http.Request) { ) } +// show 422 missing xsrf token +func missingxsrf(rw http.ResponseWriter, r *http.Request) { + responseError(rw, r, + 422, + "
The page you have requested is forbidden."+ + "
Perhaps you are here because:"+ + "

    "+ + "
    '_xsrf' argument missing from POST"+ + "
", + ) +} + +// show 417 invalid xsrf token +func invalidxsrf(rw http.ResponseWriter, r *http.Request) { + responseError(rw, r, + 417, + "
The page you have requested is forbidden."+ + "
Perhaps you are here because:"+ + "

    "+ + "
    expected XSRF not found"+ + "
", + ) +} + // show 404 not found error. func notFound(rw http.ResponseWriter, r *http.Request) { responseError(rw, r, diff --git a/hooks.go b/hooks.go index b5a5e6c5..edf1485a 100644 --- a/hooks.go +++ b/hooks.go @@ -32,6 +32,8 @@ func registerDefaultErrorHandler() error { "502": badGateway, "503": serviceUnavailable, "504": gatewayTimeout, + "417": invalidxsrf, + "422": missingxsrf, } for e, h := range m { if _, ok := ErrorMaps[e]; !ok { From 88d07058a56974878603fe26758fb5690a6a7d00 Mon Sep 17 00:00:00 2001 From: Yang Luo Date: Fri, 19 May 2017 22:19:31 +0800 Subject: [PATCH 32/32] Fix the new repo address for casbin. --- .travis.yml | 2 +- plugins/authz/authz.go | 4 ++-- plugins/authz/authz_test.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index aa88a44d..2937e6e8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,7 +34,7 @@ install: - go get github.com/cloudflare/golz4 - go get github.com/gogo/protobuf/proto - go get github.com/Knetic/govaluate - - go get github.com/hsluoyz/casbin + - go get github.com/casbin/casbin - go get -u honnef.co/go/tools/cmd/gosimple - go get -u github.com/mdempsky/unconvert - go get -u github.com/gordonklaus/ineffassign diff --git a/plugins/authz/authz.go b/plugins/authz/authz.go index 709a613a..9dc0db76 100644 --- a/plugins/authz/authz.go +++ b/plugins/authz/authz.go @@ -17,7 +17,7 @@ // import( // "github.com/astaxie/beego" // "github.com/astaxie/beego/plugins/authz" -// "github.com/hsluoyz/casbin" +// "github.com/casbin/casbin" // ) // // func main(){ @@ -42,7 +42,7 @@ package authz import ( "github.com/astaxie/beego" "github.com/astaxie/beego/context" - "github.com/hsluoyz/casbin" + "github.com/casbin/casbin" "net/http" ) diff --git a/plugins/authz/authz_test.go b/plugins/authz/authz_test.go index 4003582c..49aed84c 100644 --- a/plugins/authz/authz_test.go +++ b/plugins/authz/authz_test.go @@ -18,7 +18,7 @@ import ( "github.com/astaxie/beego" "github.com/astaxie/beego/context" "github.com/astaxie/beego/plugins/auth" - "github.com/hsluoyz/casbin" + "github.com/casbin/casbin" "net/http" "net/http/httptest" "testing"