commit 8489fc1556330a4e37f16356d0e704cd082ed50b Author: bryanqiu Date: Mon Dec 5 23:45:59 2022 +0800 initial version diff --git a/api.go b/api.go new file mode 100644 index 0000000..6c1631c --- /dev/null +++ b/api.go @@ -0,0 +1,82 @@ +package api + +import ( + "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" + "qoobing.com/gomod/log" + "qoobing.com/gomod/str" +) + +var ( + validate = validator.New() + requestLogidGetter = defaultLogidGetter + errCodeUnknownError = -10000 + errCodeUnimplementApi = -20401 +) + +type Engine struct { + *gin.Engine +} + +type BaseWithEnoOutput struct { + Eno int `json:"eno"` + Err string `json:"err"` +} + +type BaseWithCodeOutput struct { + Code int `json:"code"` + Msg string `json:"msg"` +} + +type BaseWithErrCodeOutput struct { + ErrCode int `json:"errCode"` + ErrMsg string `json:"errMsg"` +} + +func NewEngine() *Engine { + gin.SetMode(gin.ReleaseMode) + e := gin.New() + e.Use(middlewareMyApiEngine()) + return &Engine{e} +} + +func (e *Engine) POST(uri string, handler HandlerFunc) { + e.Engine.POST(uri, handlerWrapper(handler)) +} + +// middlewareMyApiEngine myapi engine middleware +func middlewareMyApiEngine() gin.HandlerFunc { + return func(c *gin.Context) { + // Step 1. set logid + id := requestLogidGetter(c) + log.SetLogid(id) + defer log.Cleanup() + + // Step 2. init *api.Context + cc := New(c) + c.Set("cc", cc) + + // Step 3. do request + c.Next() + } +} + +// defaultLogidGetter get logid from "X-Request-Id" or create a new one. +func defaultLogidGetter(c *gin.Context) (logid string) { + if logid = c.Request.Header.Get("X-Request-Id"); logid != "" { + return logid + } + if logid = str.GetRandomNumString(13); logid != "" { + return logid + } + panic("unreachable code") +} + +func InitErrCodes(errcodes map[string]int) { + if code, ok := errcodes["ErrCodeUnknownError"]; ok { + errCodeUnknownError = code + } + if code, ok := errcodes["ErrCodeUnimplementApi"]; ok { + errCodeUnknownError = code + } +} diff --git a/context.go b/context.go new file mode 100644 index 0000000..91c486e --- /dev/null +++ b/context.go @@ -0,0 +1,160 @@ +package api + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "reflect" + "runtime/debug" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" + "qoobing.com/gomod/log" + "qoobing.com/gomod/str" +) + +type Context struct { + *gin.Context + + AppId string //current request appid, must not be empty + UserId uint64 //current request userid, maybe 0 if user unknown + + resultentry string + httpstatus int + starttime time.Time +} + +func New(c *gin.Context) *Context { + return &Context{ + Context: c, + httpstatus: 200, + starttime: time.Now(), + AppId: "", + UserId: 0, + } +} + +func (c *Context) TryRecover() { + if err := recover(); err != nil { + calldepth := 3 + stack := strings.Replace(string(debug.Stack()), "\n", "\n== ", -1) + stack = str.SkipLine(stack, calldepth*2+1) + + t := reflect.TypeOf(err) + switch t.Kind() { + case reflect.String: + log.Fatalf("panic err:%v. Panic stack:\n%s", err, stack) + c.RESULT_ERROR(errCodeUnknownError, "unknown error") + default: + log.Fatalf("panic err type: %s:%s. Panic stack:\n%s", t.Name(), t.String(), stack) + c.RESULT_ERROR(errCodeUnknownError, "unknown error") + } + } +} + +func (c *Context) BindInput(i interface{}) (err error) { + req := c.Request + log.DebugfWithDepth(1, "[%s|%s|%s]", req.Method, req.Host, req.RequestURI) + + logstr := fmt.Sprintf("input:[have not initialized]") + defer func(plogstr *string) { + log.DebugfWithDepth(2, "%s", *plogstr) + }(&logstr) + + err = c.ShouldBind(i) + if err != nil { + b, _ := json.Marshal(i) + logstr = fmt.Sprintf("input:%s binderror:%s", string(b), err.Error()) + return err + } + + err = validate.Struct(i) + if err != nil { + outputstr := "" + for _, err := range err.(validator.ValidationErrors) { + b, _ := json.Marshal(i) + logstr = fmt.Sprintf("input:%s validateerror:%s", string(b), err.Error()) + outputstr = fmt.Sprintf("invalide field: [%s(%s)]", err.Field(), err.Tag()) + return errors.New(outputstr) + } + panic("Unreachable Code") + } + + // recode input parameters. + b, _ := json.Marshal(i) + logstr = fmt.Sprintf("input:%s", string(b)) + return nil +} + +func (c *Context) SetCookie(cookie *http.Cookie) { + if cookie.Path == "" { + cookie.Path = "/" + } + http.SetCookie(c.Writer, cookie) +} + +func (c *Context) RESULT(output interface{}) error { + var t = reflect.TypeOf(output) + defer func(o *interface{}) { + b, err := json.Marshal(o) + if err != nil { + return + } + calldepth := 0 + switch c.resultentry { + case "RESULT": + calldepth = 0 + case "RESULT_ERROR": + calldepth = 1 + case "RESULT_PARAMETER_ERROR": + calldepth = 2 + } + //// cut log if too long ////// begin ////////////// + if len(b) > 1024 { + //no := retag.Convert(*o, retag.NewView("json", "!logignore")) + //if nb, err := json.Marshal(no); err != nil { + // return + //} else { + // b = nb + //} + } + //// cut log if too long ////// end //////////////// + cost := time.Now().Sub(c.starttime).Milliseconds() + noticeStr := fmt.Sprintf("cost: %dms, output: '%s'", cost, string(b)) + log.NoticefWithDepth(calldepth, noticeStr) + }(&output) + + if _, ok := t.FieldByName("ErrCode"); !ok { + errstr := "Result MUST have 'ErrCode' field" + output = BaseWithErrCodeOutput{errCodeUnknownError, errstr} + c.IndentedJSON(c.httpstatus, output) + return errors.New(errstr) + } + if _, ok := t.FieldByName("ErrMsg"); !ok { + errstr := "Result MUST have 'ErrMsg' field" + output = BaseWithErrCodeOutput{errCodeUnknownError, errstr} + c.IndentedJSON(c.httpstatus, output) + return errors.New(errstr) + } + c.IndentedJSON(c.httpstatus, output) + return nil +} + +func (c *Context) RESULT_ERROR(eno int, err string) error { + if c.resultentry == "" { + c.resultentry = "RESULT_ERROR" + } + + result := BaseWithErrCodeOutput{eno, err} + return c.RESULT(result) +} + +func (c *Context) RESULT_PARAMETER_ERROR(err string) error { + if c.resultentry == "" { + c.resultentry = "RESULT_PARAMETER_ERROR" + } + return c.RESULT_ERROR(errCodeUnknownError, err) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..181b00a --- /dev/null +++ b/go.mod @@ -0,0 +1,12 @@ +module qoobing.com/gomod/api + +go 1.16 + +require ( + github.com/gin-gonic/gin v1.8.1 + github.com/go-playground/validator/v10 v10.11.1 + github.com/tylerb/gls v0.0.0-20150407001822-e606233f194d // indirect + github.com/tylerb/is v2.1.4+incompatible // indirect + qoobing.com/gomod/log v1.1.0 + qoobing.com/gomod/str v1.0.3 +) diff --git a/routes.go b/routes.go new file mode 100644 index 0000000..8766ec1 --- /dev/null +++ b/routes.go @@ -0,0 +1,49 @@ +package api + +import ( + "reflect" + + "github.com/gin-gonic/gin" + "qoobing.com/gomod/log" +) + +type routes struct { + gin.IRoutes +} + +type HandlerFunc func(*Context) error + +type IRoutes interface { + POST(uri string, handler HandlerFunc) +} + +func (r routes) POST(uri string, handler HandlerFunc) { + r.IRoutes.POST(uri, handlerWrapper(handler)) +} + +func DumpHandler(c *Context) error { + return c.RESULT_ERROR(errCodeUnimplementApi, "api not implemented") +} + +func ErrorHandler(c *gin.Context, errcode int, errmsg string) { + handler := handlerWrapper(func(c *Context) error { + return c.RESULT_ERROR(errcode, errmsg) + }) + handler(c) +} + +func handlerWrapper(handler HandlerFunc) gin.HandlerFunc { + return func(c *gin.Context) { + if icc, ok := c.Get("cc"); !ok { + log.Errorf("Unreachable Code: can not get cc(*api.Context) from *gin.Context") + } else if cc, ok := icc.(*Context); !ok { + log.Debugf("Unreachable Code: cc from *gin.Context is type of:[%s]", reflect.TypeOf(icc).String()) + log.Errorf("Unreachable Code: cc from *gin.Context is not type of *api.Context") + } else { + defer cc.TryRecover() + cc.AppId = c.GetString("appid") + cc.UserId = c.GetUint64("userid") + handler(cc) + } + } +}