├── test ├── views │ └── mini │ │ ├── test.log │ │ ├── test.txt │ │ └── testActionResult.html ├── init_test.go ├── panic_test.go ├── https_test.go ├── param_name_test.go ├── area_test.go ├── getHttpContext_test.go ├── _apidoc_test.go ├── newApplicationBuilder_test.go ├── benchmark_run_test.go ├── error_test.go ├── routes_test.go ├── run_test.go ├── routeRegexp_test.go ├── validate_test.go ├── cors_test.go ├── response_test.go ├── inject_test.go ├── jwt_test.go ├── websocket_test.go ├── actionResult_test.go ├── controller_test.go └── request_test.go ├── minimal ├── init.go ├── handleMiddleware.go └── register.go ├── check ├── icheck.go └── check.go ├── context ├── iMiddleware.go ├── iFilter.go ├── iHttpSession.go ├── httpData.go ├── httpCookies.go ├── httpURL.go ├── httpRequest.go ├── httpResponse.go ├── httpJwt.go ├── httpRoute.go ├── httpContext.go └── regexp.go ├── controller ├── action.go ├── iActionFilter.go ├── iController.go ├── baseController.go ├── init.go ├── handleMiddleware.go └── register.go ├── action ├── IActionResult.go ├── contentResult.go ├── fileContentResult.go ├── redirectToRouteResult.go ├── imageResult.go ├── callResult.go └── viewResult.go ├── iSessionMiddlewareCreat.go ├── middleware ├── urlRewriting.go ├── websocket.go ├── cors.go ├── routing.go ├── exception.go ├── http.go ├── validate.go ├── apiResponse.go └── initMiddleware.go ├── .gitignore ├── session-redis ├── middleware │ └── session.go ├── sessionMiddlewareCreat.go ├── module.go ├── go.mod └── context │ └── httpSession.go ├── filter └── jwtFilter.go ├── module.go ├── .github ├── ISSUE_TEMPLATE │ └── feature_request.md └── workflows │ └── test.yml ├── jwt.go ├── websocket ├── handleMiddleware.go ├── socketHandler.go ├── register.go ├── webSocketContext.go └── baseContext.go ├── route.go ├── LICENSE ├── go.mod ├── httpHandler.go ├── README.md ├── run.go ├── apiDoc.go ├── serveMux.go └── applicationBuilder.go /test/views/mini/test.log: -------------------------------------------------------------------------------- 1 | ddd -------------------------------------------------------------------------------- /test/views/mini/test.txt: -------------------------------------------------------------------------------- 1 | bbb -------------------------------------------------------------------------------- /test/views/mini/testActionResult.html: -------------------------------------------------------------------------------- 1 | aaa -------------------------------------------------------------------------------- /minimal/init.go: -------------------------------------------------------------------------------- 1 | package minimal 2 | 3 | func Init() { 4 | } 5 | -------------------------------------------------------------------------------- /test/init_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | func init() { 4 | //fs.Initialize[webapi.Module]("demo") 5 | } 6 | -------------------------------------------------------------------------------- /check/icheck.go: -------------------------------------------------------------------------------- 1 | package check 2 | 3 | // ICheck 当json反序化到dto时,将调用ICheck接口 4 | type ICheck interface { 5 | // Check 自定义检查 6 | Check() 7 | } 8 | -------------------------------------------------------------------------------- /context/iMiddleware.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | // IMiddleware 中间件 4 | type IMiddleware interface { 5 | Invoke(httpContext *HttpContext) 6 | } 7 | -------------------------------------------------------------------------------- /controller/action.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | type Action struct { 4 | Method string // POST/GET/PUT/DELETE 5 | Params string // 函数的入参名称 6 | } 7 | -------------------------------------------------------------------------------- /action/IActionResult.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import "github.com/farseer-go/webapi/context" 4 | 5 | type IResult interface { 6 | ExecuteResult(httpContext *context.HttpContext) 7 | } 8 | -------------------------------------------------------------------------------- /iSessionMiddlewareCreat.go: -------------------------------------------------------------------------------- 1 | package webapi 2 | 3 | import "github.com/farseer-go/webapi/context" 4 | 5 | type ISessionMiddlewareCreat interface { 6 | // Create 创建Session中间件 7 | Create() context.IMiddleware 8 | } 9 | -------------------------------------------------------------------------------- /controller/iActionFilter.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | // IActionFilter 过滤器 4 | type IActionFilter interface { 5 | // OnActionExecuting Action执行前 6 | OnActionExecuting() 7 | // OnActionExecuted Action执行后 8 | OnActionExecuted() 9 | } 10 | -------------------------------------------------------------------------------- /context/iFilter.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | // IFilter 过滤器 4 | type IFilter interface { 5 | // OnActionExecuting 页面执行前执行 6 | OnActionExecuting(httpContext *HttpContext) 7 | // OnActionExecuted 页面执行后执行 8 | OnActionExecuted(httpContext *HttpContext) 9 | } 10 | -------------------------------------------------------------------------------- /test/panic_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/farseer-go/webapi" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestPanic(t *testing.T) { 10 | assert.Panics(t, func() { 11 | webapi.RegisterPOST("/", func() {}) 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /context/iHttpSession.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | type IHttpSession interface { 4 | // GetValue 获取Session 5 | GetValue(name string) any 6 | // SetValue 设置Session 7 | SetValue(name string, val any) 8 | // Remove 删除Session 9 | Remove(name string) 10 | // Clear 清空Session 11 | Clear() 12 | } 13 | -------------------------------------------------------------------------------- /controller/iController.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/farseer-go/collections" 5 | ) 6 | 7 | // 得到IController接口的所有方法名称 8 | var lstControllerMethodName collections.List[string] 9 | 10 | type IController interface { 11 | getAction() map[string]Action // 获取Action设置 12 | } 13 | -------------------------------------------------------------------------------- /middleware/urlRewriting.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import "github.com/farseer-go/webapi/context" 4 | 5 | type UrlRewriting struct { 6 | context.IMiddleware 7 | } 8 | 9 | func (receiver *UrlRewriting) Invoke(httpContext *context.HttpContext) { 10 | receiver.IMiddleware.Invoke(httpContext) 11 | } 12 | -------------------------------------------------------------------------------- /context/httpData.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import "github.com/farseer-go/collections" 4 | 5 | type HttpData struct { 6 | value collections.Dictionary[string, any] 7 | } 8 | 9 | // Get 获取值 10 | func (receiver *HttpData) Get(key string) any { 11 | return receiver.value.GetValue(key) 12 | } 13 | 14 | // Set 设置值 15 | func (receiver *HttpData) Set(key string, val any) { 16 | receiver.value.Add(key, val) 17 | } 18 | -------------------------------------------------------------------------------- /test/https_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/farseer-go/fs" 5 | "github.com/farseer-go/fs/configure" 6 | "github.com/farseer-go/webapi" 7 | "testing" 8 | ) 9 | 10 | func TestHttps(t *testing.T) { 11 | fs.Initialize[webapi.Module]("demo") 12 | configure.SetDefault("Log.Component.webapi", true) 13 | 14 | webapi.UsePprof() 15 | webapi.UseTLS("", "") 16 | go webapi.Run(":80443") 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | .idea/* 17 | go.sum 18 | go.work.sum 19 | go.work 20 | /test/log/ 21 | *.DS_Store -------------------------------------------------------------------------------- /controller/baseController.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/farseer-go/webapi/context" 5 | ) 6 | 7 | type BaseController struct { 8 | *context.HttpContext // 上下文 9 | Action map[string]Action // 设置每个Action参数 10 | } 11 | 12 | func (receiver *BaseController) getAction() map[string]Action { 13 | if receiver.Action == nil { 14 | receiver.Action = make(map[string]Action) 15 | } 16 | return receiver.Action 17 | } 18 | -------------------------------------------------------------------------------- /action/contentResult.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "github.com/farseer-go/webapi/context" 5 | ) 6 | 7 | // ContentResult 返回响应内容 8 | type ContentResult struct { 9 | content string 10 | } 11 | 12 | func (receiver ContentResult) ExecuteResult(httpContext *context.HttpContext) { 13 | httpContext.Response.WriteString(receiver.content) 14 | } 15 | 16 | // Content 内容 17 | func Content(content string) IResult { 18 | return ContentResult{ 19 | content: content, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /session-redis/middleware/session.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | webapiContext "github.com/farseer-go/webapi/context" 5 | "github.com/farseer-go/webapi/session-redis/context" 6 | ) 7 | 8 | type Session struct { 9 | webapiContext.IMiddleware 10 | } 11 | 12 | func (receiver *Session) Invoke(httpContext *webapiContext.HttpContext) { 13 | httpContext.Session = context.InitSession(httpContext.Response.W, httpContext.Request.R) 14 | receiver.IMiddleware.Invoke(httpContext) 15 | } 16 | -------------------------------------------------------------------------------- /filter/jwtFilter.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "github.com/farseer-go/fs/exception" 5 | "github.com/farseer-go/webapi/context" 6 | ) 7 | 8 | type JwtFilter struct { 9 | } 10 | 11 | func (receiver JwtFilter) OnActionExecuting(httpContext *context.HttpContext) { 12 | if !httpContext.Jwt.Valid() { 13 | exception.ThrowWebExceptionf(context.InvalidStatusCode, context.InvalidMessage) 14 | } 15 | } 16 | 17 | func (receiver JwtFilter) OnActionExecuted(httpContext *context.HttpContext) { 18 | } 19 | -------------------------------------------------------------------------------- /session-redis/sessionMiddlewareCreat.go: -------------------------------------------------------------------------------- 1 | package session_redis 2 | 3 | import ( 4 | webapiContext "github.com/farseer-go/webapi/context" 5 | "github.com/farseer-go/webapi/session-redis/context" 6 | "github.com/farseer-go/webapi/session-redis/middleware" 7 | ) 8 | 9 | type SessionMiddlewareCreat struct { 10 | } 11 | 12 | // Create 创建Session中间件 13 | func (receiver *SessionMiddlewareCreat) Create() webapiContext.IMiddleware { 14 | go context.ClearSession() 15 | return &middleware.Session{} 16 | } 17 | -------------------------------------------------------------------------------- /test/param_name_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/farseer-go/fs" 5 | "github.com/farseer-go/fs/configure" 6 | "github.com/farseer-go/webapi" 7 | "testing" 8 | ) 9 | 10 | func hello(pageSize int, pageIndex int) (int, int) { 11 | return pageSize, pageIndex 12 | } 13 | 14 | func TestParamName(t *testing.T) { 15 | fs.Initialize[webapi.Module]("demo") 16 | configure.SetDefault("Log.Component.webapi", true) 17 | webapi.RegisterDELETE("/cors/test", hello) 18 | webapi.UseCors() 19 | go webapi.Run(":8080") 20 | } 21 | -------------------------------------------------------------------------------- /controller/init.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/farseer-go/collections" 5 | "reflect" 6 | ) 7 | 8 | func Init() { 9 | // 获取IController的方法列表 10 | getIControllerMethodNames() 11 | } 12 | 13 | // 获取IController的方法列表 14 | func getIControllerMethodNames() { 15 | iControllerType := reflect.TypeOf((*IController)(nil)).Elem() 16 | lstControllerMethodName = collections.NewList[string]() 17 | for i := 0; i < iControllerType.NumMethod(); i++ { 18 | lstControllerMethodName.Add(iControllerType.Method(i).Name) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /action/fileContentResult.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "github.com/farseer-go/webapi/context" 5 | "os" 6 | ) 7 | 8 | // FileContentResult 返回文件内容 9 | type FileContentResult struct { 10 | filePath string 11 | } 12 | 13 | func (receiver FileContentResult) ExecuteResult(httpContext *context.HttpContext) { 14 | file, _ := os.ReadFile(receiver.filePath) 15 | httpContext.Response.Write(file) 16 | } 17 | 18 | // FileContent 文件 19 | func FileContent(filePath string) IResult { 20 | return FileContentResult{ 21 | filePath: filePath, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /middleware/websocket.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/farseer-go/webapi/context" 5 | "net/http" 6 | ) 7 | 8 | type Websocket struct { 9 | context.IMiddleware 10 | } 11 | 12 | func (receiver *Websocket) Invoke(httpContext *context.HttpContext) { 13 | httpContext.Response.SetHttpCode(http.StatusOK) 14 | httpContext.Response.SetStatusCode(http.StatusOK) 15 | 16 | // 下一步:exceptionMiddleware 17 | receiver.IMiddleware.Invoke(httpContext) 18 | 19 | _ = httpContext.WebsocketConn.WriteClose(httpContext.Response.GetHttpCode()) 20 | } 21 | -------------------------------------------------------------------------------- /module.go: -------------------------------------------------------------------------------- 1 | package webapi 2 | 3 | import ( 4 | "github.com/farseer-go/fs/modules" 5 | "github.com/farseer-go/webapi/context" 6 | "github.com/farseer-go/webapi/controller" 7 | "github.com/farseer-go/webapi/minimal" 8 | ) 9 | 10 | type Module struct { 11 | } 12 | 13 | func (module Module) DependsModule() []modules.FarseerModule { 14 | return nil 15 | } 16 | 17 | func (module Module) PreInitialize() { 18 | controller.Init() 19 | minimal.Init() 20 | defaultApi = NewApplicationBuilder() 21 | } 22 | 23 | func (module Module) PostInitialize() { 24 | context.InitJwt() 25 | } 26 | -------------------------------------------------------------------------------- /action/redirectToRouteResult.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "github.com/farseer-go/webapi/context" 5 | "net/http" 6 | ) 7 | 8 | // RedirectToRouteResult 重定向功能 9 | type RedirectToRouteResult struct { 10 | url string 11 | } 12 | 13 | func (receiver RedirectToRouteResult) ExecuteResult(httpContext *context.HttpContext) { 14 | httpContext.Response.AddHeader("Location", receiver.url) 15 | httpContext.Response.SetHttpCode(http.StatusFound) 16 | } 17 | 18 | // Redirect 重定向 19 | func Redirect(url string) IResult { 20 | return RedirectToRouteResult{ 21 | url: url, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /session-redis/module.go: -------------------------------------------------------------------------------- 1 | package session_redis 2 | 3 | import ( 4 | "github.com/farseer-go/fs/configure" 5 | "github.com/farseer-go/fs/modules" 6 | "github.com/farseer-go/webapi" 7 | "github.com/farseer-go/webapi/session-redis/context" 8 | ) 9 | 10 | type Module struct { 11 | } 12 | 13 | func (module Module) DependsModule() []modules.FarseerModule { 14 | return []modules.FarseerModule{webapi.Module{}} 15 | } 16 | 17 | func (module Module) PreInitialize() { 18 | sessionTimeout := configure.GetInt("Webapi.Session.Age") 19 | if sessionTimeout > 0 { 20 | context.SessionTimeout = sessionTimeout 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /action/imageResult.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "bytes" 5 | "github.com/farseer-go/webapi/context" 6 | ) 7 | 8 | // ImageResult 处理图片 9 | type ImageResult struct { 10 | buffer *bytes.Buffer 11 | imageType string 12 | } 13 | 14 | func (receiver *ImageResult) ExecuteResult(httpContext *context.HttpContext) { 15 | httpContext.Response.Write(receiver.buffer.Bytes()) 16 | httpContext.Response.SetHeader("Content-Type", receiver.imageType) 17 | } 18 | 19 | // Image 返回图片格式 20 | func Image(buffer *bytes.Buffer, imageType string) IResult { 21 | return &ImageResult{ 22 | buffer: buffer, 23 | imageType: imageType, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /check/check.go: -------------------------------------------------------------------------------- 1 | package check 2 | 3 | import ( 4 | "github.com/farseer-go/fs/exception" 5 | "strings" 6 | ) 7 | 8 | // IsEmpty 当val为empty时,抛出异常 9 | func IsEmpty(val string, statusCode int, err string) { 10 | if strings.TrimSpace(val) == "" { 11 | exception.ThrowWebException(statusCode, err) 12 | } 13 | } 14 | 15 | // IsTrue 当val为true时,抛出异常 16 | func IsTrue(val bool, statusCode int, err string) { 17 | if val { 18 | exception.ThrowWebException(statusCode, err) 19 | } 20 | } 21 | 22 | // IsFalse 当val为false时,抛出异常 23 | func IsFalse(val bool, statusCode int, err string) { 24 | if !val { 25 | exception.ThrowWebException(statusCode, err) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /jwt.go: -------------------------------------------------------------------------------- 1 | package webapi 2 | 3 | import ( 4 | "github.com/golang-jwt/jwt/v5" 5 | ) 6 | 7 | var jwtKey []byte // jwt key 8 | var jwtKeyType string // key type 9 | var jwtKeyMethod jwt.SigningMethod // sign method 10 | 11 | //type MyCustomClaims struct { 12 | // Foo string `json:"foo"` 13 | // jwt.RegisteredClaims 14 | //} 15 | 16 | //// NewJwtToken 生成Jwt Token 17 | //func NewJwtToken(claims map[string]any) (string, error) { 18 | // //jwt.MapClaims{ 19 | // // "foo": "bar", 20 | // // "nbf": time.Date(2015, 10, 10, 12, 0, 0, 0, time.UTC).Unix(), 21 | // //} 22 | // token := jwt.NewWithClaims(jwtKeyMethod, jwt.MapClaims(claims)) 23 | // if len(jwtKey) == 0 { 24 | // return token.SigningString() 25 | // } 26 | // return token.SignedString(jwtKey) 27 | //} 28 | -------------------------------------------------------------------------------- /test/area_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "testing" 7 | "time" 8 | 9 | "github.com/farseer-go/fs" 10 | "github.com/farseer-go/fs/configure" 11 | "github.com/farseer-go/webapi" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestArea(t *testing.T) { 16 | fs.Initialize[webapi.Module]("demo") 17 | configure.SetDefault("Log.Component.webapi", true) 18 | webapi.Area("api/1.0", func() { 19 | webapi.RegisterGET("/mini/test", func() string { 20 | return "ok" 21 | }) 22 | }) 23 | go webapi.Run(":8087") 24 | time.Sleep(10 * time.Millisecond) 25 | 26 | rsp, _ := http.Get("http://127.0.0.1:8087/api/1.0/mini/test") 27 | body, _ := io.ReadAll(rsp.Body) 28 | _ = rsp.Body.Close() 29 | assert.Equal(t, "ok", string(body)) 30 | } 31 | -------------------------------------------------------------------------------- /websocket/handleMiddleware.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "github.com/farseer-go/webapi/context" 5 | "reflect" 6 | ) 7 | 8 | type HandleMiddleware struct { 9 | } 10 | 11 | func (receiver HandleMiddleware) Invoke(httpContext *context.HttpContext) { 12 | // 执行过滤器OnActionExecuting 13 | for i := 0; i < len(httpContext.Route.Filters); i++ { 14 | httpContext.Route.Filters[i].OnActionExecuting(httpContext) 15 | } 16 | 17 | // 实现了check.ICheck(必须放在过滤器之后执行) 18 | httpContext.RequestParamCheck() 19 | 20 | // 调用action 21 | callValues := reflect.ValueOf(httpContext.Route.Action).Call(httpContext.Request.Params) 22 | httpContext.Response.SetValues(callValues...) 23 | 24 | // 执行过滤器OnActionExecuted 25 | for i := 0; i < len(httpContext.Route.Filters); i++ { 26 | httpContext.Route.Filters[i].OnActionExecuted(httpContext) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/getHttpContext_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/farseer-go/fs" 5 | "github.com/farseer-go/fs/core" 6 | "github.com/farseer-go/webapi" 7 | "github.com/stretchr/testify/assert" 8 | "net/http" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestGetHttpContext(t *testing.T) { 14 | fs.Initialize[webapi.Module]("demo") 15 | 16 | webapi.RegisterPOST("/getHttpContext", func() string { 17 | return webapi.GetHttpContext().ContentType 18 | }) 19 | webapi.UseApiResponse() 20 | go webapi.Run(":8089") 21 | time.Sleep(10 * time.Millisecond) 22 | 23 | t.Run("getHttpContext", func(t *testing.T) { 24 | rsp, _ := http.Post("http://127.0.0.1:8089/getHttpContext", "application/json", nil) 25 | apiResponse := core.NewApiResponseByReader[string](rsp.Body) 26 | _ = rsp.Body.Close() 27 | assert.Equal(t, "application/json", apiResponse.Data) 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /test/_apidoc_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/farseer-go/fs" 6 | "github.com/farseer-go/webapi" 7 | "testing" 8 | ) 9 | 10 | func TestApiDoc(t *testing.T) { 11 | fs.Initialize[webapi.Module]("demo") 12 | webapi.UseApiResponse() 13 | webapi.PrintRoute() 14 | webapi.UseApiDoc() 15 | 16 | webapi.RegisterPOST("/dto", func(req pageSizeRequest) string { 17 | webapi.GetHttpContext().Response.SetMessage(200, "测试成功") 18 | return fmt.Sprintf("hello world pageSize=%d,pageIndex=%d", req.PageSize, req.PageIndex) 19 | }) 20 | 21 | webapi.RegisterGET("/empty", func() any { 22 | return pageSizeRequest{PageSize: 3, PageIndex: 2} 23 | }) 24 | 25 | webapi.RegisterPUT("/multiParam", func(pageSize int, pageIndex int) pageSizeRequest { 26 | return pageSizeRequest{ 27 | PageSize: pageSize, 28 | PageIndex: pageIndex, 29 | } 30 | }, "page_size", "pageIndex") 31 | 32 | go webapi.Run("") 33 | } 34 | -------------------------------------------------------------------------------- /middleware/cors.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import "github.com/farseer-go/webapi/context" 4 | 5 | type Cors struct { 6 | context.IMiddleware 7 | } 8 | 9 | func (receiver *Cors) Invoke(httpContext *context.HttpContext) { 10 | httpContext.Response.AddHeader("Access-Control-Allow-Headers", httpContext.Header.GetValue("Access-Control-Request-Headers")) 11 | httpContext.Response.AddHeader("Access-Control-Allow-Methods", httpContext.Header.GetValue("Access-Control-Request-Methods")) 12 | httpContext.Response.AddHeader("Access-Control-Allow-Credentials", "true") 13 | httpContext.Response.AddHeader("Access-Control-Max-Age", "86400") 14 | 15 | if httpContext.Header.GetValue("Origin") != "" { 16 | httpContext.Response.AddHeader("Access-Control-Allow-Origin", httpContext.Header.GetValue("Origin")) 17 | } 18 | 19 | if httpContext.Method == "OPTIONS" { 20 | httpContext.Response.SetHttpCode(204) 21 | return 22 | } 23 | receiver.IMiddleware.Invoke(httpContext) 24 | } 25 | -------------------------------------------------------------------------------- /middleware/routing.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "bytes" 5 | "github.com/farseer-go/webapi/context" 6 | ) 7 | 8 | type routing struct { 9 | context.IMiddleware 10 | } 11 | 12 | func (receiver *routing) Invoke(httpContext *context.HttpContext) { 13 | // 检查method 14 | if httpContext.Route.Schema != "ws" && httpContext.Method != "OPTIONS" && !httpContext.Route.Method.Contains(httpContext.Method) { 15 | // 响应码 16 | httpContext.Response.Reject(405, "405 Method NotAllowed") 17 | return 18 | } 19 | 20 | // 解析Body 21 | buf := new(bytes.Buffer) 22 | _, _ = buf.ReadFrom(httpContext.Request.R.Body) 23 | httpContext.Request.BodyString = buf.String() 24 | httpContext.Request.BodyBytes = buf.Bytes() 25 | 26 | // 解析请求的参数 27 | httpContext.Request.ParseQuery() 28 | httpContext.Request.ParseForm() 29 | httpContext.URI.Query = httpContext.Request.Query 30 | 31 | // 转换成Handle函数需要的参数 32 | httpContext.Request.Params = httpContext.ParseParams() 33 | 34 | receiver.IMiddleware.Invoke(httpContext) 35 | } 36 | -------------------------------------------------------------------------------- /minimal/handleMiddleware.go: -------------------------------------------------------------------------------- 1 | package minimal 2 | 3 | import ( 4 | "github.com/farseer-go/fs/container" 5 | "github.com/farseer-go/fs/trace" 6 | "github.com/farseer-go/webapi/context" 7 | "reflect" 8 | ) 9 | 10 | type HandleMiddleware struct { 11 | } 12 | 13 | func (receiver HandleMiddleware) Invoke(httpContext *context.HttpContext) { 14 | // 执行过滤器OnActionExecuting 15 | for i := 0; i < len(httpContext.Route.Filters); i++ { 16 | httpContext.Route.Filters[i].OnActionExecuting(httpContext) 17 | } 18 | 19 | // 实现了check.ICheck(必须放在过滤器之后执行) 20 | httpContext.RequestParamCheck() 21 | 22 | traceDetail := container.Resolve[trace.IManager]().TraceHand("执行路由") 23 | // 调用action 24 | callValues := reflect.ValueOf(httpContext.Route.Action).Call(httpContext.Request.Params) 25 | httpContext.Response.SetValues(callValues...) 26 | traceDetail.End(nil) 27 | 28 | // 执行过滤器OnActionExecuted 29 | for i := 0; i < len(httpContext.Route.Filters); i++ { 30 | httpContext.Route.Filters[i].OnActionExecuted(httpContext) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /action/callResult.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "github.com/farseer-go/collections" 5 | "github.com/farseer-go/fs/parse" 6 | "github.com/farseer-go/webapi/context" 7 | ) 8 | 9 | // callResult 默认调用Action结果 10 | type callResult struct { 11 | } 12 | 13 | func NewCallResult() callResult { 14 | return callResult{} 15 | } 16 | 17 | func (receiver callResult) ExecuteResult(httpContext *context.HttpContext) { 18 | // 只有一个返回值 19 | if len(httpContext.Response.Body) == 1 { 20 | responseBody := httpContext.Response.Body[0] 21 | // 基本类型直接转string 22 | if httpContext.Route.IsGoBasicType { 23 | httpContext.Response.Write([]byte(parse.ToString(responseBody))) 24 | } else { // dto 25 | httpContext.Response.WriteJson(responseBody) 26 | httpContext.Response.SetHeader("Content-Type", "application/json") 27 | } 28 | return 29 | } 30 | 31 | // 多个返回值,则转成数组Json 32 | lst := collections.NewListAny() 33 | for i := 0; i < len(httpContext.Response.Body); i++ { 34 | lst.Add(httpContext.Response.Body[i]) 35 | } 36 | httpContext.Response.WriteJson(lst) 37 | } 38 | -------------------------------------------------------------------------------- /test/newApplicationBuilder_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net/http" 7 | "testing" 8 | "time" 9 | 10 | "github.com/farseer-go/fs" 11 | "github.com/farseer-go/fs/configure" 12 | "github.com/farseer-go/fs/snc" 13 | "github.com/farseer-go/webapi" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | func TestNewApplicationBuilder(t *testing.T) { 18 | fs.Initialize[webapi.Module]("demo") 19 | configure.SetDefault("Log.Component.webapi", true) 20 | 21 | server := webapi.NewApplicationBuilder() 22 | server.RegisterPOST("/mini/test", func() {}) 23 | go server.Run(":8083") 24 | time.Sleep(10 * time.Millisecond) 25 | 26 | t.Run("mini/test2:8083", func(t *testing.T) { 27 | sizeRequest := pageSizeRequest{PageSize: 10, PageIndex: 2} 28 | marshal, _ := snc.Marshal(sizeRequest) 29 | rsp, _ := http.Post("http://127.0.0.1:8083/mini/test", "application/json", bytes.NewReader(marshal)) 30 | body, _ := io.ReadAll(rsp.Body) 31 | _ = rsp.Body.Close() 32 | assert.Equal(t, "", string(body)) 33 | assert.Equal(t, 200, rsp.StatusCode) 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /route.go: -------------------------------------------------------------------------------- 1 | package webapi 2 | 3 | import ( 4 | "github.com/farseer-go/webapi/context" 5 | "github.com/farseer-go/webapi/filter" 6 | ) 7 | 8 | // Route 路由配置 9 | type Route struct { 10 | Method string // Method类型(GET|POST|PUT|DELETE) 11 | Url string // 路由地址 12 | Action any // Handle 13 | Message string // api返回的StatusMessage 14 | Filters []context.IFilter // 过滤器(对单个路由的执行单元) 15 | Params []string // Handle的入参 16 | // Summary string // api的描述 17 | } 18 | 19 | // UseJwt 使用Jwt 20 | func (receiver Route) UseJwt() Route { 21 | receiver.Filters = append(receiver.Filters, filter.JwtFilter{}) 22 | return receiver 23 | } 24 | 25 | // POST 使用POST 26 | func (receiver Route) POST() Route { 27 | receiver.Method = "POST" 28 | return receiver 29 | } 30 | 31 | // GET 使用GET 32 | func (receiver Route) GET() Route { 33 | receiver.Method = "GET" 34 | return receiver 35 | } 36 | 37 | // Filter 添加过滤器 38 | func (receiver Route) Filter(filter context.IFilter) Route { 39 | receiver.Filters = append(receiver.Filters, filter) 40 | return receiver 41 | 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Test 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v3 21 | with: 22 | go-version: 1.23.3 23 | 24 | - name: Go Mod Tidy 25 | run: go mod tidy 26 | 27 | # - name: Build 28 | # run: go build -v ./... 29 | 30 | # - name: Test 31 | # run: go test -v ./... 32 | 33 | - name: Run coverage 34 | #run: cd test && go test -race -coverprofile=coverage.txt -covermode=atomic 35 | run: go test -race -covermode=count -coverprofile=coverage.txt -covermode=atomic -run="^Test" -coverpkg=$(go list ./... | grep -v "/test" | tr '\n' ',') ./test 36 | - name: Upload coverage to Codecov 37 | uses: codecov/codecov-action@v3 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 farseer-go 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/farseer-go/webapi 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.8 6 | 7 | require ( 8 | github.com/farseer-go/collections v0.17.3 9 | github.com/farseer-go/fs v0.17.3 10 | github.com/farseer-go/utils v0.17.3 11 | github.com/go-playground/locales v0.14.1 12 | github.com/go-playground/universal-translator v0.18.1 13 | github.com/go-playground/validator/v10 v10.27.0 14 | github.com/golang-jwt/jwt/v5 v5.2.3 15 | github.com/stretchr/testify v1.10.0 16 | github.com/timandy/routine v1.1.5 17 | golang.org/x/net v0.42.0 18 | ) 19 | 20 | require ( 21 | github.com/davecgh/go-spew v1.1.1 // indirect 22 | github.com/gabriel-vasile/mimetype v1.4.9 // indirect 23 | github.com/json-iterator/go v1.1.12 // indirect 24 | github.com/leodido/go-urn v1.4.0 // indirect 25 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 26 | github.com/modern-go/reflect2 v1.0.2 // indirect 27 | github.com/pmezard/go-difflib v1.0.0 // indirect 28 | golang.org/x/crypto v0.40.0 // indirect 29 | golang.org/x/sys v0.34.0 // indirect 30 | golang.org/x/text v0.27.0 // indirect 31 | gopkg.in/yaml.v3 v3.0.1 // indirect 32 | ) 33 | -------------------------------------------------------------------------------- /websocket/socketHandler.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "github.com/farseer-go/fs/asyncLocal" 5 | "github.com/farseer-go/fs/container" 6 | "github.com/farseer-go/fs/trace" 7 | "github.com/farseer-go/webapi/context" 8 | "golang.org/x/net/websocket" 9 | ) 10 | 11 | func SocketHandler(route *context.HttpRoute) websocket.Handler { 12 | return func(conn *websocket.Conn) { 13 | // 调用action 14 | // 解析报文、组装httpContext 15 | httpContext := context.NewHttpContext(route, nil, conn.Request()) 16 | httpContext.SetWebsocket(conn) 17 | 18 | // InitContext 初始化同一协程上下文,避免在同一协程中多次初始化 19 | asyncLocal.InitContext() 20 | 21 | // 创建链路追踪上下文 22 | trackContext := container.Resolve[trace.IManager]().EntryWebSocket(httpContext.URI.Host, httpContext.URI.Url, httpContext.Header.ToMap(), httpContext.URI.GetRealIp()) 23 | trackContext.SetBody(httpContext.Request.BodyString, httpContext.Response.GetHttpCode(), string(httpContext.Response.BodyBytes), nil) 24 | container.Resolve[trace.IManager]().Push(trackContext, nil) 25 | //httpContext.Data.Set("Trace", trackContext) 26 | 27 | // 设置到routine,可用于任意子函数获取 28 | context.RoutineHttpContext.Set(httpContext) 29 | // 执行第一个中间件 30 | route.HttpMiddleware.Invoke(httpContext) 31 | asyncLocal.Release() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/benchmark_run_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http" 7 | "testing" 8 | "time" 9 | 10 | "github.com/farseer-go/fs" 11 | "github.com/farseer-go/fs/configure" 12 | "github.com/farseer-go/fs/snc" 13 | "github.com/farseer-go/webapi" 14 | ) 15 | 16 | // BenchmarkRun-12 4434 304151 ns/op 22731 B/op 202 allocs/op(优化前) 17 | // BenchmarkRun-12 4432 239972 ns/op 22758 B/op 203 allocs/op(第一次优化:HttpResponse.Body改为[]any) 18 | func BenchmarkRun(b *testing.B) { 19 | fs.Initialize[webapi.Module]("demo") 20 | configure.SetDefault("Log.Component.webapi", true) 21 | 22 | webapi.RegisterPOST("/dto", func(req pageSizeRequest) string { 23 | webapi.GetHttpContext().Response.SetMessage(200, "测试成功") 24 | return fmt.Sprintf("hello world pageSize=%d,pageIndex=%d", req.PageSize, req.PageIndex) 25 | }) 26 | 27 | webapi.UseApiResponse() 28 | go webapi.Run(":8094") 29 | time.Sleep(10 * time.Millisecond) 30 | b.ReportAllocs() 31 | sizeRequest := pageSizeRequest{PageSize: 10, PageIndex: 2} 32 | marshal, _ := snc.Marshal(sizeRequest) 33 | 34 | for i := 0; i < b.N; i++ { 35 | rsp, _ := http.Post("http://127.0.0.1:8094/dto", "application/json", bytes.NewReader(marshal)) 36 | _ = rsp.Body.Close() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /httpHandler.go: -------------------------------------------------------------------------------- 1 | package webapi 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/farseer-go/fs/asyncLocal" 7 | "github.com/farseer-go/fs/container" 8 | "github.com/farseer-go/fs/trace" 9 | "github.com/farseer-go/webapi/context" 10 | ) 11 | 12 | func HttpHandler(route *context.HttpRoute) http.HandlerFunc { 13 | return func(w http.ResponseWriter, r *http.Request) { 14 | // InitContext 初始化同一协程上下文,避免在同一协程中多次初始化 15 | asyncLocal.InitContext() 16 | // 解析报文、组装httpContext 17 | httpContext := context.NewHttpContext(route, w, r) 18 | // 创建链路追踪上下文 19 | trackContext := container.Resolve[trace.IManager]().EntryWebApi(httpContext.URI.Host, httpContext.URI.Url, httpContext.Method, httpContext.ContentType, httpContext.Header.ToMap(), httpContext.URI.GetRealIp()) 20 | // 记录出入参 21 | defer func() { 22 | trackContext.SetBody(httpContext.Request.BodyString, httpContext.Response.GetHttpCode(), string(httpContext.Response.BodyBytes), httpContext.ResponseHeader.ToMap()) 23 | container.Resolve[trace.IManager]().Push(trackContext, nil) 24 | }() 25 | httpContext.Data.Set("Trace", trackContext) 26 | 27 | // 设置到routine,可用于任意子函数获取 28 | context.RoutineHttpContext.Set(httpContext) 29 | // 执行第一个中间件 30 | route.HttpMiddleware.Invoke(httpContext) 31 | asyncLocal.Release() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /middleware/exception.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/farseer-go/fs/core" 7 | "github.com/farseer-go/fs/exception" 8 | "github.com/farseer-go/webapi/context" 9 | "golang.org/x/net/websocket" 10 | ) 11 | 12 | // exception 异常中间件(默认加载) 13 | type exceptionMiddleware struct { 14 | context.IMiddleware 15 | } 16 | 17 | func (receiver *exceptionMiddleware) Invoke(httpContext *context.HttpContext) { 18 | // exceptionMiddleware 与 ApiResponse 中间件是互诉的。 19 | exception.Try(func() { 20 | // 下一步:routing 21 | receiver.IMiddleware.Invoke(httpContext) 22 | }).CatchWebException(func(exp exception.WebException) { 23 | // ws协议先主动发一条消息,然后立即关闭 24 | if httpContext.WebsocketConn != nil { 25 | _ = websocket.JSON.Send(httpContext.WebsocketConn, core.ApiResponseStringError(exp.Message, exp.StatusCode)) 26 | } 27 | // 响应码 28 | httpContext.Response.Write([]byte(exp.Message)) 29 | httpContext.Response.SetHttpCode(exp.StatusCode) 30 | }).CatchException(func(exp any) { 31 | // ws协议先主动发一条消息,然后立即关闭 32 | if httpContext.WebsocketConn != nil { 33 | _ = websocket.JSON.Send(httpContext.WebsocketConn, core.ApiResponseStringError(httpContext.Exception.Error(), http.StatusInternalServerError)) 34 | } 35 | 36 | // 响应码 37 | httpContext.Response.Write([]byte(httpContext.Exception.Error())) 38 | httpContext.Response.SetHttpCode(http.StatusInternalServerError) 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /session-redis/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/farseer-go/webapi/session-redis 2 | 3 | go 1.22 4 | 5 | toolchain go1.23.3 6 | 7 | require ( 8 | github.com/farseer-go/cache v0.16.0 9 | github.com/farseer-go/cacheMemory v0.16.0 10 | github.com/farseer-go/fs v0.16.0 11 | github.com/farseer-go/redis v0.16.0 12 | github.com/farseer-go/webapi v0.16.0 13 | ) 14 | 15 | require ( 16 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 17 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 18 | github.com/farseer-go/collections v0.16.0 // indirect 19 | github.com/farseer-go/mapper v0.16.0 // indirect 20 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 21 | github.com/go-playground/locales v0.14.1 // indirect 22 | github.com/go-playground/universal-translator v0.18.1 // indirect 23 | github.com/go-playground/validator/v10 v10.26.0 // indirect 24 | github.com/go-redis/redis/v8 v8.11.5 // indirect 25 | github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 26 | github.com/json-iterator/go v1.1.12 // indirect 27 | github.com/leodido/go-urn v1.4.0 // indirect 28 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 29 | github.com/modern-go/reflect2 v1.0.2 // indirect 30 | github.com/timandy/routine v1.1.5 // indirect 31 | golang.org/x/crypto v0.36.0 // indirect 32 | golang.org/x/net v0.38.0 // indirect 33 | golang.org/x/sys v0.31.0 // indirect 34 | golang.org/x/text v0.23.0 // indirect 35 | gopkg.in/yaml.v3 v3.0.1 // indirect 36 | ) 37 | -------------------------------------------------------------------------------- /middleware/http.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "github.com/farseer-go/collections" 8 | "github.com/farseer-go/webapi/action" 9 | "github.com/farseer-go/webapi/context" 10 | ) 11 | 12 | // Http HTTP报文响应中间件(默认加载) 13 | type Http struct { 14 | context.IMiddleware 15 | } 16 | 17 | func (receiver *Http) Invoke(httpContext *context.HttpContext) { 18 | httpContext.Response.SetHttpCode(http.StatusOK) 19 | httpContext.Response.SetStatusCode(http.StatusOK) 20 | 21 | // 下一步:exceptionMiddleware 22 | receiver.IMiddleware.Invoke(httpContext) 23 | 24 | // 说明没有中间件对输出做处理 25 | if len(httpContext.Response.BodyBytes) == 0 && len(httpContext.Response.Body) > 0 { 26 | // IActionResult 27 | if httpContext.IsActionResult() { 28 | actionResult := httpContext.Response.Body[0].(action.IResult) 29 | actionResult.ExecuteResult(httpContext) 30 | } else { 31 | // 则转成callResult 32 | action.NewCallResult().ExecuteResult(httpContext) 33 | } 34 | } 35 | 36 | // 输出返回值 37 | httpContext.Response.W.WriteHeader(httpContext.Response.GetHttpCode()) 38 | 39 | // 响应header 40 | rspHeader := collections.NewDictionary[string, string]() 41 | for k, v := range httpContext.Response.W.Header() { 42 | rspHeader.Add(k, strings.Join(v, ";")) 43 | } 44 | httpContext.ResponseHeader = rspHeader.ToReadonlyDictionary() 45 | 46 | // 写入Response流 47 | if len(httpContext.Response.BodyBytes) > 0 { 48 | _, _ = httpContext.Response.W.Write(httpContext.Response.BodyBytes) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/error_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/farseer-go/fs" 5 | "github.com/farseer-go/fs/core" 6 | "github.com/farseer-go/fs/exception" 7 | "github.com/farseer-go/webapi" 8 | "github.com/stretchr/testify/assert" 9 | "net/http" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func TestError(t *testing.T) { 15 | fs.Initialize[webapi.Module]("demo") 16 | webapi.RegisterPOST("/error/1", func() { 17 | exception.ThrowWebException(501, "s501") 18 | }) 19 | 20 | webapi.RegisterPOST("/error/2", func() { 21 | exception.ThrowException("s500") 22 | }) 23 | webapi.UseApiResponse() 24 | go webapi.Run(":8081") 25 | time.Sleep(10 * time.Millisecond) 26 | 27 | t.Run("error/1", func(t *testing.T) { 28 | rsp, _ := http.Post("http://127.0.0.1:8081/error/1", "application/json", nil) 29 | apiResponse := core.NewApiResponseByReader[any](rsp.Body) 30 | _ = rsp.Body.Close() 31 | assert.Equal(t, 501, apiResponse.StatusCode) 32 | assert.Equal(t, "s501", apiResponse.StatusMessage) 33 | assert.Equal(t, false, apiResponse.Status) 34 | assert.Equal(t, 200, rsp.StatusCode) 35 | }) 36 | 37 | t.Run("error/2", func(t *testing.T) { 38 | rsp, _ := http.Post("http://127.0.0.1:8081/error/2", "application/json", nil) 39 | apiResponse := core.NewApiResponseByReader[any](rsp.Body) 40 | _ = rsp.Body.Close() 41 | assert.Equal(t, 500, apiResponse.StatusCode) 42 | assert.Equal(t, "s500", apiResponse.StatusMessage) 43 | assert.Equal(t, false, apiResponse.Status) 44 | assert.Equal(t, 200, rsp.StatusCode) 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /test/routes_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "testing" 9 | "time" 10 | 11 | "github.com/farseer-go/fs" 12 | "github.com/farseer-go/fs/configure" 13 | "github.com/farseer-go/fs/snc" 14 | "github.com/farseer-go/webapi" 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | func TestRoutes(t *testing.T) { 19 | fs.Initialize[webapi.Module]("demo") 20 | configure.SetDefault("Log.Component.webapi", true) 21 | 22 | webapi.RegisterRoutes(webapi.Route{Url: "/mini/test1", Method: "POST|GET", Action: func(req pageSizeRequest) string { 23 | return fmt.Sprintf("hello world pageSize=%d,pageIndex=%d", req.PageSize, req.PageIndex) 24 | }}) 25 | go webapi.Run(":8095") 26 | time.Sleep(10 * time.Millisecond) 27 | 28 | t.Run("mini/test1:8095-POST", func(t *testing.T) { 29 | sizeRequest := pageSizeRequest{PageSize: 10, PageIndex: 2} 30 | marshal, _ := snc.Marshal(sizeRequest) 31 | rsp, _ := http.Post("http://127.0.0.1:8095/mini/test1", "application/json", bytes.NewReader(marshal)) 32 | body, _ := io.ReadAll(rsp.Body) 33 | _ = rsp.Body.Close() 34 | assert.Equal(t, "hello world pageSize=10,pageIndex=2", string(body)) 35 | assert.Equal(t, 200, rsp.StatusCode) 36 | }) 37 | t.Run("mini/test1:8095-GET", func(t *testing.T) { 38 | rsp, _ := http.Get("http://127.0.0.1:8095/mini/test1?page_size=10&PageIndex=2") 39 | body, _ := io.ReadAll(rsp.Body) 40 | _ = rsp.Body.Close() 41 | assert.Equal(t, "hello world pageSize=10,pageIndex=2", string(body)) 42 | assert.Equal(t, 200, rsp.StatusCode) 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /context/httpCookies.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | ) 7 | 8 | // 初始化Cookies 9 | func initCookies(w http.ResponseWriter, r *http.Request) *HttpCookies { 10 | return &HttpCookies{ 11 | w: w, 12 | r: r, 13 | } 14 | } 15 | 16 | type HttpCookies struct { 17 | w http.ResponseWriter 18 | r *http.Request 19 | } 20 | 21 | // Get 获取Cookie 22 | func (r *HttpCookies) Get(name string) *http.Cookie { 23 | cookie, _ := r.r.Cookie(name) 24 | return cookie 25 | } 26 | 27 | // GetValue 获取Cookie 28 | func (r *HttpCookies) GetValue(name string) string { 29 | cookie, _ := r.r.Cookie(name) 30 | if cookie == nil { 31 | return "" 32 | } 33 | return cookie.Value 34 | } 35 | 36 | // SetValue 设置Cookie 37 | func (r *HttpCookies) SetValue(name string, val string) { 38 | http.SetCookie(r.w, &http.Cookie{ 39 | Name: name, 40 | Value: val, 41 | Path: "/", 42 | HttpOnly: false, 43 | }) 44 | } 45 | 46 | // SetSuretyValue 设置Cookie安全值,将不允许脚本读取该值(HttpOnly) 47 | func (r *HttpCookies) SetSuretyValue(name string, val string) { 48 | http.SetCookie(r.w, &http.Cookie{ 49 | Name: name, 50 | Value: val, 51 | Path: "/", 52 | HttpOnly: true, 53 | }) 54 | } 55 | 56 | // SetCookie 设置Cookie 57 | func (r *HttpCookies) SetCookie(cookie *http.Cookie) { 58 | http.SetCookie(r.w, cookie) 59 | } 60 | 61 | // Remove 删除Cookie 62 | func (r *HttpCookies) Remove(name string) { 63 | http.SetCookie(r.w, &http.Cookie{ 64 | Name: name, 65 | Value: "", 66 | HttpOnly: true, 67 | MaxAge: -1, 68 | Expires: time.Unix(1, 0), 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /context/httpURL.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "github.com/farseer-go/fs/parse" 8 | ) 9 | 10 | type HttpURL struct { 11 | Url string // 请求地址 http(s)://xxx.xxx.xxx/xxx 12 | Path string // 请求地址 13 | RemoteAddr string // 客户端IP端口 14 | X_Forwarded_For string // 客户端IP端口 15 | X_Real_Ip string // 客户端IP端口 16 | Host string // 请求的Host主机头 17 | Proto string // http协议 18 | RequestURI string 19 | QueryString string 20 | Query map[string]any 21 | R *http.Request 22 | } 23 | 24 | //func (receiver *HttpURL) ParseQuery() { 25 | // for k, v := range receiver.R.URL.Query() { 26 | // key := strings.ToLower(k) 27 | // receiver.Query[key] = strings.Join(v, "&") 28 | // } 29 | //} 30 | 31 | // GetRealIp 获取真实IP 32 | func (receiver *HttpURL) GetRealIp() string { 33 | ip := receiver.X_Real_Ip 34 | if ip == "" { 35 | ip = strings.Split(receiver.X_Forwarded_For, ",")[0] 36 | } 37 | if ip == "" { 38 | ip = receiver.RemoteAddr 39 | } 40 | return strings.Split(ip, ":")[0] 41 | } 42 | 43 | // GetRealIpPort 获取真实IP、Port 44 | func (receiver *HttpURL) GetRealIpPort() (string, int) { 45 | ips := []string{receiver.X_Real_Ip, strings.Split(receiver.X_Forwarded_For, ",")[0], receiver.RemoteAddr} 46 | for _, ip := range ips { 47 | if ipPorts := strings.Split(ip, ":"); len(ipPorts) == 2 { 48 | return ipPorts[0], parse.ToInt(ipPorts[1]) 49 | } 50 | } 51 | 52 | // 如果没有找到IP和端口,则返回RemoteAddr的IP部分和0端口 53 | return strings.Split(receiver.RemoteAddr, ":")[0], 0 54 | } 55 | -------------------------------------------------------------------------------- /test/run_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/farseer-go/fs" 5 | "github.com/farseer-go/fs/configure" 6 | "github.com/farseer-go/fs/core" 7 | "github.com/farseer-go/webapi" 8 | "github.com/farseer-go/webapi/middleware" 9 | "github.com/stretchr/testify/assert" 10 | "net/http" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | type pageSizeRequest struct { 16 | PageSize int `json:"Page_size"` 17 | PageIndex int 18 | noExported string //测试不导出字段 19 | } 20 | 21 | func Hello2() any { 22 | return pageSizeRequest{PageSize: 3, PageIndex: 2} 23 | } 24 | 25 | func TestRun(t *testing.T) { 26 | fs.Initialize[webapi.Module]("demo") 27 | configure.SetDefault("Log.Component.webapi", true) 28 | webapi.RegisterRoutes(webapi.Route{Url: "/mini/hello2", Method: "GET", Action: Hello2}) 29 | 30 | assert.Panics(t, func() { 31 | webapi.RegisterRoutes(webapi.Route{Url: "/mini/hello3", Method: "GET", Action: Hello2, Params: []string{"aaa"}}) 32 | }) 33 | webapi.UseWebApi() 34 | webapi.UseStaticFiles() 35 | webapi.UseApiResponse() 36 | webapi.RegisterMiddleware(&middleware.UrlRewriting{}) 37 | 38 | go webapi.Run(":8093") 39 | time.Sleep(10 * time.Millisecond) 40 | 41 | t.Run("mini/hello2", func(t *testing.T) { 42 | rsp, _ := http.Get("http://127.0.0.1:8093/mini/hello2") 43 | apiResponse := core.NewApiResponseByReader[pageSizeRequest](rsp.Body) 44 | _ = rsp.Body.Close() 45 | assert.Equal(t, 3, apiResponse.Data.PageSize) 46 | assert.Equal(t, 2, apiResponse.Data.PageIndex) 47 | assert.Equal(t, 200, rsp.StatusCode) 48 | assert.Equal(t, 200, apiResponse.StatusCode) 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /test/routeRegexp_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/farseer-go/fs" 5 | "github.com/farseer-go/fs/configure" 6 | "github.com/farseer-go/fs/core" 7 | "github.com/farseer-go/webapi" 8 | "github.com/stretchr/testify/assert" 9 | "net/http" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func TestRouteRegexp(t *testing.T) { 15 | fs.Initialize[webapi.Module]("demo") 16 | configure.SetDefault("Log.Component.webapi", true) 17 | 18 | webapi.RegisterGET("/mini/{pageSize}-{pageIndex}", func(pageSize int, pageIndex int) (int, int) { 19 | return pageSize, pageIndex 20 | }) 21 | webapi.RegisterPOST("/mini/{pageSize}/{pageIndex}", func(pageSize int, pageIndex int) (int, int) { 22 | return pageSize, pageIndex 23 | }) 24 | 25 | webapi.UseApiResponse() 26 | 27 | go webapi.Run(":8091") 28 | time.Sleep(10 * time.Millisecond) 29 | 30 | t.Run("/mini/{pageSize}-{pageIndex}-get", func(t *testing.T) { 31 | req, _ := http.NewRequest("GET", "http://127.0.0.1:8091/mini/15-6", nil) 32 | rsp, _ := http.DefaultClient.Do(req) 33 | apiResponse := core.NewApiResponseByReader[[]int](rsp.Body) 34 | _ = rsp.Body.Close() 35 | assert.Equal(t, []int{15, 6}, apiResponse.Data) 36 | assert.Equal(t, 200, rsp.StatusCode) 37 | }) 38 | 39 | t.Run("/mini/{pageSize}/{pageIndex}-post", func(t *testing.T) { 40 | req, _ := http.NewRequest("POST", "http://127.0.0.1:8091/mini/15/6", nil) 41 | rsp, _ := http.DefaultClient.Do(req) 42 | apiResponse := core.NewApiResponseByReader[[]int](rsp.Body) 43 | _ = rsp.Body.Close() 44 | assert.Equal(t, []int{15, 6}, apiResponse.Data) 45 | assert.Equal(t, 200, rsp.StatusCode) 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /middleware/validate.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "errors" 5 | "github.com/farseer-go/collections" 6 | "github.com/farseer-go/fs/exception" 7 | "github.com/farseer-go/webapi/context" 8 | "github.com/go-playground/locales/zh" 9 | ut "github.com/go-playground/universal-translator" 10 | "github.com/go-playground/validator/v10" 11 | zhTranslations "github.com/go-playground/validator/v10/translations/zh" 12 | "reflect" 13 | ) 14 | 15 | var validate *validator.Validate 16 | var trans ut.Translator 17 | 18 | type Validate struct { 19 | context.IMiddleware 20 | } 21 | 22 | // InitValidate 初始化字段验证 23 | func InitValidate() { 24 | validate = validator.New(validator.WithRequiredStructEnabled()) 25 | trans, _ = ut.New(zh.New()).GetTranslator("zh") 26 | _ = zhTranslations.RegisterDefaultTranslations(validate, trans) 27 | //注册一个函数,获取struct tag里自定义的label作为字段名 28 | validate.RegisterTagNameFunc(func(fld reflect.StructField) string { 29 | name := fld.Tag.Get("label") 30 | return name 31 | }) 32 | } 33 | 34 | func (receiver *Validate) Invoke(httpContext *context.HttpContext) { 35 | // 验证dto 36 | if httpContext.Route.RequestParamIsModel { 37 | lstError := collections.NewList[string]() 38 | err := validate.Struct(httpContext.Request.Params[0].Interface()) 39 | if err != nil { 40 | var validationErrors validator.ValidationErrors 41 | errors.As(err, &validationErrors) 42 | for _, validationError := range validationErrors { 43 | lstError.Add(validationError.Translate(trans)) 44 | } 45 | } 46 | if lstError.Count() > 0 { 47 | exception.ThrowWebException(403, lstError.ToString(",")) 48 | return 49 | } 50 | } 51 | receiver.IMiddleware.Invoke(httpContext) 52 | } 53 | -------------------------------------------------------------------------------- /test/validate_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "testing" 9 | "time" 10 | 11 | "github.com/farseer-go/fs" 12 | "github.com/farseer-go/fs/configure" 13 | "github.com/farseer-go/fs/snc" 14 | "github.com/farseer-go/webapi" 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | type ValidateRequest struct { 19 | Name string `validate:"required" label:"账号"` 20 | Age int `validate:"gte=0,lte=100" label:"年龄"` 21 | } 22 | 23 | func TestValidate(t *testing.T) { 24 | fs.Initialize[webapi.Module]("demo") 25 | configure.SetDefault("Log.Component.webapi", true) 26 | webapi.RegisterRoutes(webapi.Route{Url: "/Validate", Method: "POST", Action: func(dto ValidateRequest) string { 27 | return fmt.Sprintf("%+v", dto) 28 | }}) 29 | webapi.UseValidate() 30 | go webapi.Run(":8092") 31 | time.Sleep(10 * time.Millisecond) 32 | 33 | t.Run("/Validate error", func(t *testing.T) { 34 | sizeRequest := ValidateRequest{Name: "", Age: 200} 35 | marshal, _ := snc.Marshal(sizeRequest) 36 | rsp, _ := http.Post("http://127.0.0.1:8092/Validate", "application/json", bytes.NewReader(marshal)) 37 | body, _ := io.ReadAll(rsp.Body) 38 | _ = rsp.Body.Close() 39 | assert.Equal(t, "账号为必填字段,年龄必须小于或等于100", string(body)) 40 | assert.Equal(t, 403, rsp.StatusCode) 41 | }) 42 | 43 | t.Run("/Validate success", func(t *testing.T) { 44 | sizeRequest := ValidateRequest{Name: "steden", Age: 37} 45 | marshal, _ := snc.Marshal(sizeRequest) 46 | rsp, _ := http.Post("http://127.0.0.1:8092/Validate", "application/json", bytes.NewReader(marshal)) 47 | body, _ := io.ReadAll(rsp.Body) 48 | _ = rsp.Body.Close() 49 | assert.Equal(t, "{Name:steden Age:37}", string(body)) 50 | assert.Equal(t, 200, rsp.StatusCode) 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /test/cors_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net/http" 7 | "testing" 8 | "time" 9 | 10 | "github.com/farseer-go/fs" 11 | "github.com/farseer-go/fs/configure" 12 | "github.com/farseer-go/fs/snc" 13 | "github.com/farseer-go/webapi" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | func TestCors(t *testing.T) { 18 | fs.Initialize[webapi.Module]("demo") 19 | configure.SetDefault("Log.Component.webapi", true) 20 | server := webapi.NewApplicationBuilder() 21 | server.RegisterDELETE("/cors/test", func(pageSize int, pageIndex int) (int, int) { 22 | return pageSize, pageIndex 23 | }, "page_Size", "pageIndex") 24 | server.UseCors() 25 | go server.Run(":8080") 26 | time.Sleep(10 * time.Millisecond) 27 | 28 | t.Run("/cors/test:8080", func(t *testing.T) { 29 | sizeRequest := pageSizeRequest{PageSize: 10, PageIndex: 2} 30 | marshal, _ := snc.Marshal(sizeRequest) 31 | req, _ := http.NewRequest("DELETE", "http://127.0.0.1:8080/cors/test", bytes.NewReader(marshal)) 32 | req.Header.Set("Content-Type", "application/json") 33 | rsp, _ := http.DefaultClient.Do(req) 34 | body, _ := io.ReadAll(rsp.Body) 35 | _ = rsp.Body.Close() 36 | assert.Equal(t, "[10,2]", string(body)) 37 | assert.Equal(t, 200, rsp.StatusCode) 38 | }) 39 | 40 | t.Run("/cors/test:8080-OPTIONS", func(t *testing.T) { 41 | sizeRequest := pageSizeRequest{PageSize: 10, PageIndex: 2} 42 | marshal, _ := snc.Marshal(sizeRequest) 43 | req, _ := http.NewRequest("OPTIONS", "http://127.0.0.1:8080/cors/test", bytes.NewReader(marshal)) 44 | req.Header.Set("Content-Type", "application/json") 45 | req.Header.Set("Origin", "localhost") 46 | rsp, _ := http.DefaultClient.Do(req) 47 | body, _ := io.ReadAll(rsp.Body) 48 | _ = rsp.Body.Close() 49 | assert.Equal(t, "", string(body)) 50 | assert.Equal(t, "localhost", rsp.Header.Get("Access-Control-Allow-Origin")) 51 | assert.Equal(t, "true", rsp.Header.Get("Access-Control-Allow-Credentials")) 52 | assert.Equal(t, 204, rsp.StatusCode) 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /context/httpRequest.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "net/url" 7 | "reflect" 8 | "strings" 9 | 10 | "github.com/farseer-go/fs/snc" 11 | ) 12 | 13 | type HttpRequest struct { 14 | Body io.ReadCloser 15 | BodyString string 16 | BodyBytes []byte 17 | Form map[string]any 18 | Query map[string]any 19 | Params []reflect.Value // 转换成Handle函数需要的参数 20 | R *http.Request 21 | } 22 | 23 | // jsonToMap 将json转成map类型 24 | func (r *HttpRequest) jsonToMap() map[string]any { 25 | mapVal := make(map[string]any) 26 | //_ = json.Unmarshal(r.BodyBytes, &mapVal) 27 | // d := json.NewDecoder(bytes.NewReader(r.BodyBytes)) 28 | // d.UseNumber() 29 | // _ = d.Decode(&mapVal) 30 | snc.Unmarshal(r.BodyBytes, &mapVal) 31 | 32 | // 将Key转小写 33 | for k, v := range mapVal { 34 | kLower := strings.ToLower(k) 35 | if k != kLower { 36 | delete(mapVal, k) 37 | mapVal[kLower] = v 38 | } 39 | } 40 | return mapVal 41 | } 42 | 43 | // ParseForm 解析来自form的值 44 | func (r *HttpRequest) ParseForm() { 45 | for k, v := range r.R.Form { 46 | key := strings.ToLower(k) 47 | r.Form[key] = strings.Join(v, "&") 48 | r.Query[key] = strings.Join(v, "&") 49 | } 50 | 51 | // multipart/form-data提交的数据在Body中 52 | if r.BodyString != "" { 53 | parseQuery, _ := url.ParseQuery(r.BodyString) 54 | for key, value := range parseQuery { 55 | key = strings.ToLower(key) 56 | if len(value) > 0 { 57 | r.Form[key] = strings.Join(value, ",") 58 | r.Query[key] = strings.Join(value, ",") 59 | } 60 | } 61 | //formValues := strings.Split(r.BodyString, "&") 62 | //for _, value := range formValues { 63 | // kv := strings.Split(value, "=") 64 | // if len(kv) > 1 { 65 | // key := strings.ToLower(kv[0]) 66 | // r.Form[key] = kv[1] 67 | // r.Query[key] = kv[1] 68 | // } 69 | //} 70 | } 71 | } 72 | 73 | // ParseQuery 解析来自url的值 74 | func (r *HttpRequest) ParseQuery() { 75 | for k, v := range r.R.URL.Query() { 76 | key := strings.ToLower(k) 77 | r.Query[key] = strings.Join(v, "&") 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /test/response_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "bytes" 5 | 6 | "fmt" 7 | "net/http" 8 | "testing" 9 | "time" 10 | 11 | "github.com/farseer-go/fs" 12 | "github.com/farseer-go/fs/configure" 13 | "github.com/farseer-go/fs/core" 14 | "github.com/farseer-go/fs/snc" 15 | "github.com/farseer-go/webapi" 16 | "github.com/stretchr/testify/assert" 17 | ) 18 | 19 | func TestResponse(t *testing.T) { 20 | fs.Initialize[webapi.Module]("demo") 21 | configure.SetDefault("Log.Component.webapi", true) 22 | 23 | webapi.RegisterDELETE("/multiResponse", func(pageSize int, pageIndex int) (int, int) { 24 | return pageSize, pageIndex 25 | }, "page_size", "pageIndex") 26 | 27 | webapi.RegisterPOST("/basicTypeResponse", func(req pageSizeRequest) string { 28 | return fmt.Sprintf("hello world pageSize=%d,pageIndex=%d", req.PageSize, req.PageIndex) 29 | }) 30 | 31 | webapi.UseApiResponse() 32 | go webapi.Run(":8086") 33 | time.Sleep(10 * time.Millisecond) 34 | 35 | t.Run("multiResponse", func(t *testing.T) { 36 | sizeRequest := pageSizeRequest{PageSize: 10, PageIndex: 2} 37 | marshal, _ := snc.Marshal(sizeRequest) 38 | req, _ := http.NewRequest("DELETE", "http://127.0.0.1:8086/multiResponse", bytes.NewReader(marshal)) 39 | req.Header.Set("Content-Type", "application/json") 40 | rsp, _ := http.DefaultClient.Do(req) 41 | apiResponse := core.NewApiResponseByReader[[]int](rsp.Body) 42 | _ = rsp.Body.Close() 43 | assert.Equal(t, []int{10, 2}, apiResponse.Data) 44 | assert.Equal(t, 200, rsp.StatusCode) 45 | }) 46 | 47 | t.Run("basicTypeResponse", func(t *testing.T) { 48 | sizeRequest := pageSizeRequest{PageSize: 10, PageIndex: 2} 49 | marshal, _ := snc.Marshal(sizeRequest) 50 | rsp, _ := http.Post("http://127.0.0.1:8086/basicTypeResponse", "application/json", bytes.NewReader(marshal)) 51 | apiResponse := core.NewApiResponseByReader[string](rsp.Body) 52 | _ = rsp.Body.Close() 53 | assert.Equal(t, fmt.Sprintf("hello world pageSize=%d,pageIndex=%d", sizeRequest.PageSize, sizeRequest.PageIndex), apiResponse.Data) 54 | assert.Equal(t, 200, rsp.StatusCode) 55 | assert.Equal(t, 200, apiResponse.StatusCode) 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /middleware/apiResponse.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/farseer-go/collections" 9 | "github.com/farseer-go/fs/core" 10 | "github.com/farseer-go/fs/exception" 11 | "github.com/farseer-go/fs/trace" 12 | "github.com/farseer-go/webapi/context" 13 | ) 14 | 15 | type ApiResponse struct { 16 | context.IMiddleware 17 | } 18 | 19 | func (receiver *ApiResponse) Invoke(httpContext *context.HttpContext) { 20 | // ActionResult类型,不做ApiResponse解析 21 | if httpContext.IsActionResult() { 22 | receiver.IMiddleware.Invoke(httpContext) 23 | return 24 | } 25 | 26 | var apiResponse core.ApiResponse[any] 27 | catch := exception.Try(func() { 28 | receiver.IMiddleware.Invoke(httpContext) 29 | 30 | var returnVal any 31 | // 只有一个返回值 32 | bodyLength := len(httpContext.Response.Body) 33 | if bodyLength == 1 { 34 | returnVal = httpContext.Response.Body[0] 35 | } else if bodyLength > 1 { 36 | // 多个返回值,则转成数组Json 37 | lst := collections.NewListAny() 38 | for i := 0; i < bodyLength; i++ { 39 | lst.Add(httpContext.Response.Body[i]) 40 | } 41 | returnVal = lst 42 | } 43 | statusCode, statusMessage := httpContext.Response.GetStatus() 44 | apiResponse = core.Success[any](statusMessage, returnVal) 45 | apiResponse.StatusCode = statusCode 46 | apiResponse.Status = statusCode == 200 47 | }) 48 | 49 | catch.CatchWebException(func(exp exception.WebException) { 50 | apiResponse = core.Error[any](exp.Message, exp.StatusCode) 51 | }) 52 | 53 | catch.CatchRefuseException(func(exp exception.RefuseException) { 54 | apiResponse = core.Error[any](exp.Message, http.StatusInternalServerError) 55 | }) 56 | 57 | catch.CatchException(func(exp any) { 58 | // 响应码 59 | apiResponse = core.Error[any](fmt.Sprint(exp), http.StatusInternalServerError) 60 | }) 61 | 62 | traceContext := httpContext.Data.Get("Trace").(*trace.TraceContext) 63 | apiResponse.TraceId = traceContext.TraceId 64 | apiResponse.ElapsedMilliseconds = (time.Now().UnixMicro() - traceContext.StartTs) / 1000 65 | httpContext.Route.IsGoBasicType = false 66 | httpContext.Response.Body = []any{apiResponse} 67 | } 68 | -------------------------------------------------------------------------------- /websocket/register.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | 7 | "github.com/farseer-go/collections" 8 | "github.com/farseer-go/fs/color" 9 | "github.com/farseer-go/fs/flog" 10 | "github.com/farseer-go/fs/types" 11 | "github.com/farseer-go/webapi/context" 12 | "github.com/farseer-go/webapi/middleware" 13 | ) 14 | 15 | // Register 注册单个Api 16 | func Register(area string, method string, route string, actionFunc any, filters []context.IFilter, paramNames ...string) *context.HttpRoute { 17 | actionType := reflect.TypeOf(actionFunc) 18 | inParams := types.GetInParam(actionType) 19 | outParams := types.GetOutParam(actionType) 20 | 21 | if len(inParams) < 1 || !strings.HasPrefix(inParams[0].String(), "*websocket.Context[") { 22 | flog.Panicf("注册ws路由%s%s失败:%s函数入参必须为:%s", area, route, color.Red(actionType.String()), color.Blue("*websocket.Context[T any]")) 23 | } 24 | 25 | if len(outParams) != 0 { 26 | flog.Panicf("注册ws路由%s%s失败:%s函数不能有出参", area, route, color.Red(actionType.String())) 27 | } 28 | 29 | // 如果设置了方法的入参(多参数),则需要全部设置 30 | if len(paramNames) > 0 && len(paramNames) != len(inParams) { 31 | flog.Panicf("注册路由%s%s失败:%s函数入参与%s不匹配,建议重新运行fsctl -r命令", area, route, color.Red(actionType.String()), color.Blue(paramNames)) 32 | } 33 | 34 | // 入参的泛型是否为DTO模式 35 | itemTypeMethod, _ := inParams[0].MethodByName("ItemType") 36 | itemType := itemTypeMethod.Type.Out(0) 37 | isDtoModel := types.IsDtoModelIgnoreInterface([]reflect.Type{itemType}) 38 | 39 | // 添加到路由表 40 | return &context.HttpRoute{ 41 | Schema: "ws", 42 | RouteUrl: area + route, 43 | Action: actionFunc, 44 | Method: collections.NewList(strings.Split(strings.ToUpper(method), "|")...), 45 | RequestParamType: collections.NewList(inParams...), 46 | ResponseBodyType: collections.NewList(outParams...), 47 | RequestParamIsModel: isDtoModel, 48 | ResponseBodyIsModel: false, 49 | ParamNames: collections.NewList(paramNames...), 50 | HttpMiddleware: &middleware.Websocket{}, 51 | HandleMiddleware: &HandleMiddleware{}, 52 | IsGoBasicType: types.IsGoBasicType(itemType), 53 | Filters: filters, 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /minimal/register.go: -------------------------------------------------------------------------------- 1 | package minimal 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | 7 | "github.com/farseer-go/collections" 8 | "github.com/farseer-go/fs/color" 9 | "github.com/farseer-go/fs/flog" 10 | "github.com/farseer-go/fs/types" 11 | "github.com/farseer-go/webapi/check" 12 | "github.com/farseer-go/webapi/context" 13 | "github.com/farseer-go/webapi/middleware" 14 | ) 15 | 16 | // Register 注册单个Api 17 | func Register(area string, method string, route string, actionFunc any, filters []context.IFilter, paramNames ...string) *context.HttpRoute { 18 | actionType := reflect.TypeOf(actionFunc) 19 | inParams := types.GetInParam(actionType) 20 | 21 | // 如果设置了方法的入参(多参数),则需要全部设置 22 | if len(paramNames) > 0 && len(paramNames) != len(inParams) { 23 | flog.Panicf("注册路由%s%s失败:%s函数入参与%s不匹配,建议重新运行fsctl -r命令", area, route, color.Red(actionType.String()), color.Blue(paramNames)) 24 | } 25 | 26 | lstRequestParamType := collections.NewList(inParams...) 27 | lstResponseParamType := collections.NewList(types.GetOutParam(actionType)...) 28 | 29 | // 入参是否为DTO模式 30 | isDtoModel := types.IsDtoModelIgnoreInterface(inParams) 31 | // 是否实现了ICheck 32 | var requestParamIsImplCheck bool 33 | if isDtoModel { 34 | // 是否实现了check.ICheck 35 | var checker = reflect.TypeOf((*check.ICheck)(nil)).Elem() 36 | requestParamIsImplCheck = lstRequestParamType.First().Implements(checker) 37 | if !requestParamIsImplCheck { 38 | requestParamIsImplCheck = reflect.PointerTo(lstRequestParamType.First()).Implements(checker) 39 | } 40 | } 41 | 42 | // 添加到路由表 43 | return &context.HttpRoute{ 44 | Schema: "http", 45 | RouteUrl: area + route, 46 | Action: actionFunc, 47 | Method: collections.NewList(strings.Split(strings.ToUpper(method), "|")...), 48 | RequestParamType: lstRequestParamType, 49 | ResponseBodyType: lstResponseParamType, 50 | RequestParamIsImplCheck: requestParamIsImplCheck, 51 | RequestParamIsModel: isDtoModel, 52 | ResponseBodyIsModel: types.IsDtoModel(lstResponseParamType.ToArray()), 53 | ParamNames: collections.NewList(paramNames...), 54 | HttpMiddleware: &middleware.Http{}, 55 | HandleMiddleware: &HandleMiddleware{}, 56 | IsGoBasicType: types.IsGoBasicType(lstResponseParamType.First()), 57 | Filters: filters, 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /middleware/initMiddleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/farseer-go/collections" 5 | "github.com/farseer-go/webapi/context" 6 | "reflect" 7 | ) 8 | 9 | // InitMiddleware 初始化管道 10 | func InitMiddleware(lstRouteTable map[string]*context.HttpRoute, lstMiddleware collections.List[context.IMiddleware]) { 11 | for _, route := range lstRouteTable { 12 | // 系统web是没有中间件的 13 | if route.HttpMiddleware == nil { 14 | continue 15 | } 16 | // 组装管道 17 | lstPipeline := assembledPipeline(route, lstMiddleware) 18 | for middlewareIndex := 0; middlewareIndex < lstPipeline.Count(); middlewareIndex++ { 19 | // 最后一个中间件不需要再设置 20 | if middlewareIndex+1 == lstPipeline.Count() { 21 | break 22 | } 23 | curMiddleware := lstPipeline.Index(middlewareIndex) 24 | nextMiddleware := lstPipeline.Index(middlewareIndex + 1) 25 | setNextMiddleware(curMiddleware, nextMiddleware) 26 | } 27 | } 28 | } 29 | 30 | // 组装管道 31 | func assembledPipeline(route *context.HttpRoute, lstMiddleware collections.List[context.IMiddleware]) collections.List[context.IMiddleware] { 32 | // 添加系统中间件 33 | lst := collections.NewList[context.IMiddleware](route.HttpMiddleware, &exceptionMiddleware{}, &routing{}) 34 | 35 | // 添加用户自定义中间件 36 | for i := 0; i < lstMiddleware.Count(); i++ { 37 | middlewareType := reflect.TypeOf(lstMiddleware.Index(i)).Elem() 38 | if route.Schema != "ws" || (middlewareType.String() != "middleware.ApiResponse" && middlewareType.String() != "middleware.Cors") { 39 | valIns := reflect.New(middlewareType).Interface() 40 | lst.Add(valIns.(context.IMiddleware)) 41 | } 42 | } 43 | 44 | // 添加Handle中间件 45 | lst.Add(route.HandleMiddleware) 46 | 47 | // 找到cors中间件,放入到http之后(即移到第2个索引) 48 | for i := 3; i < lst.Count(); i++ { 49 | if corsMiddleware, isHaveCors := lst.Index(i).(*Cors); isHaveCors { 50 | lst.RemoveAt(i) 51 | lst.Insert(1, corsMiddleware) 52 | break 53 | } 54 | } 55 | return lst 56 | } 57 | 58 | // setNextMiddleware 设置下一个管道 59 | func setNextMiddleware(curMiddleware, nextMiddleware context.IMiddleware) { 60 | curMiddlewareValue := reflect.ValueOf(curMiddleware) 61 | // 找到next字段进行赋值下一个中间件管道 62 | for fieldIndex := 0; fieldIndex < curMiddlewareValue.Elem().NumField(); fieldIndex++ { 63 | field := curMiddlewareValue.Elem().Field(fieldIndex) 64 | if field.Type().String() == "context.IMiddleware" { 65 | field.Set(reflect.ValueOf(nextMiddleware)) 66 | break 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webapi 概述 2 | > 包:`"github.com/farseer-go/webapi"` 3 | > 4 | > 模块:`webapi.Module` 5 | 6 | 7 | - `Document` 8 | - [English](https://farseer-go.github.io/doc/#/en-us/) 9 | - [中文](https://farseer-go.github.io/doc/) 10 | - [English](https://farseer-go.github.io/doc/#/en-us/) 11 | - Source 12 | - [github](https://github.com/farseer-go/fs) 13 | 14 | 15 |  16 |  17 |  18 |  19 | [](https://codecov.io/gh/farseer-go/webapi) 20 |  21 | [](https://github.com/farseer-go/webapi/actions/workflows/test.yml) 22 |  23 | 24 | > 用于快速构建api服务,带来极简、优雅的开发体验。编写api服务时,不需要使用httpRequest、httpResponse等数据结构。 25 | 26 | webapi使用了中间件的管道模型编写,让我们加入非业务逻辑时非常简单。 27 | 28 | 包含两种风格来提供API服务: 29 | - `MinimalApi`:动态API风格(直接绑定到逻辑层) 30 | - `Mvc`:Controller、Action风格 31 | 32 | > 使用minimalApi时,甚至不需要UI层来提供API服务。 33 | 34 | ## webapi有哪些功能 35 | - 支持中间件 36 | - 入参、出参隐式绑定 37 | - 支持静态目录绑定 38 | - ActionFilter过虑器 39 | - ActionResult抽象结果 40 | - Area区域设置 41 | - MinimalApi模式 42 | - Mvc模式 43 | - HttpContext上下文 44 | - Header隐式绑定 45 | 46 | 大部份情况下,除了main需要配置webapi路由外,在你的api handle中就是一个普通的func函数,不需要依赖webapi组件。webapi会根据`func函数`的`出入参`来`隐式绑定数据`。 47 | 48 | ```go 49 | func main() { 50 | fs.Initialize[webapi.Module]("FOPS") 51 | webapi.RegisterPOST("/mini/hello1", Hello1) 52 | webapi.RegisterPOST("/mini/hello3", Hello3, "pageSize", "pageIndex") 53 | webapi.Run() 54 | } 55 | 56 | // 使用结构(DTO)来接收入参 57 | // 返回string 58 | func Hello1(req pageSizeRequest) string { 59 | return fmt.Sprintf("hello world pageSize=%d,pageIndex=%d", req.PageSize, req.PageIndex) 60 | } 61 | 62 | // 使用基础参数来接收入参 63 | // 返回pageSizeRequest结构(会自动转成json) 64 | func Hello3(pageSize int, pageIndex int) pageSizeRequest { 65 | return pageSizeRequest{ 66 | PageSize: pageSize, 67 | PageIndex: pageIndex, 68 | } 69 | } 70 | 71 | // 也可以定义一个结构,用于接收参数 72 | type pageSizeRequest struct { 73 | PageSize int 74 | PageIndex int 75 | } 76 | ``` 77 | 函数中,`出入参都会自动绑定数据` 78 | 79 | > 如果是`application/json`,则会自动被反序列化成model,如果是`x-www-form-urlencoded`,则会将每一项的key/value匹配到model字段中 80 | 81 | 可以看到,整个过程,`不需要`做`json序列化`、`httpRequest`、`httpResponse`的操作。 -------------------------------------------------------------------------------- /action/viewResult.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "github.com/farseer-go/collections" 5 | "github.com/farseer-go/webapi/context" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | // ViewResult 返回视图功能 11 | type ViewResult struct { 12 | viewName string 13 | data map[string]any 14 | } 15 | 16 | func (receiver ViewResult) ExecuteResult(httpContext *context.HttpContext) { 17 | // 默认视图,则以routeUrl为视图位置 18 | action := httpContext.URI.Path[strings.LastIndex(httpContext.URI.Path, "/")+1:] 19 | path, _ := receiver.cutPrefix(httpContext.URI.Path, "/") 20 | path = path[:len(path)-len(action)] 21 | 22 | if receiver.viewName == "" { 23 | receiver.viewName = "./views/" + path + action + ".html" 24 | } else { 25 | receiver.viewName = strings.TrimPrefix(receiver.viewName, "/") 26 | lstViewPath := collections.NewList(strings.Split(receiver.viewName, "/")...) 27 | if !strings.Contains(lstViewPath.Last(), ".") { 28 | receiver.viewName = "./views/" + path + receiver.viewName + ".html" 29 | } else { 30 | receiver.viewName = "./views/" + path + receiver.viewName 31 | } 32 | } 33 | 34 | file, _ := os.ReadFile(receiver.viewName) 35 | //if len(receiver.data) > 0 { 36 | // htmlSource := string(file) 37 | // startIndex := -1 38 | // stopIndex := -1 39 | // // 遍历html字符串 40 | // for i := 0; i < len(htmlSource); i++ { 41 | // // 查找开始标记 42 | // if i < len(htmlSource)-1 { 43 | // if htmlSource[i:1] == "${" { 44 | // startIndex = i 45 | // i++ 46 | // } 47 | // } 48 | // 49 | // // 查找结束标记 50 | // if startIndex > -1 && htmlSource[i] == '}' { 51 | // stopIndex = i 52 | // 53 | // // 找到了 54 | // htmlSource[startIndex : stopIndex-startIndex] 55 | // } 56 | // } 57 | // for k, v := range receiver.data { 58 | // htmlSource = strings.ReplaceAll(htmlSource, "${"+k+"}", parse.Convert(v, "")) 59 | // } 60 | //} else { 61 | // 62 | //} 63 | 64 | httpContext.Response.Write(file) 65 | } 66 | 67 | // CutPrefix go 1.20 68 | func (receiver ViewResult) cutPrefix(s, prefix string) (after string, found bool) { 69 | if !strings.HasPrefix(s, prefix) { 70 | return s, false 71 | } 72 | return s[len(prefix):], true 73 | } 74 | 75 | // View 视图 76 | func View(viewName ...string) IResult { 77 | var view string 78 | if len(viewName) > 0 { 79 | view = viewName[0] 80 | } 81 | return ViewResult{ 82 | viewName: view, 83 | } 84 | } 85 | 86 | // ViewData 视图 87 | func ViewData(Data map[string]any, viewName ...string) IResult { 88 | var view string 89 | if len(viewName) > 0 { 90 | view = viewName[0] 91 | } 92 | return ViewResult{ 93 | viewName: view, 94 | data: Data, 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /test/inject_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/farseer-go/fs" 5 | "github.com/farseer-go/fs/container" 6 | "github.com/farseer-go/webapi" 7 | "github.com/farseer-go/webapi/controller" 8 | "github.com/stretchr/testify/assert" 9 | "io" 10 | "net/http" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | type ITestInject interface { 16 | Call() string 17 | } 18 | 19 | type testInject struct { 20 | value string 21 | } 22 | 23 | func (receiver *testInject) Call() string { 24 | return receiver.value 25 | } 26 | 27 | type testInjectController struct { 28 | controller.BaseController 29 | } 30 | 31 | func (r *testInjectController) Hello1(val ITestInject) string { 32 | return val.Call() 33 | } 34 | 35 | // 测试注入 36 | func TestInject(t *testing.T) { 37 | fs.Initialize[webapi.Module]("demo") 38 | // 注册接口实例 39 | container.Register(func() ITestInject { 40 | return &testInject{"ok1"} 41 | }) 42 | container.Register(func() ITestInject { 43 | return &testInject{"ok2"} 44 | }, "ins2") 45 | 46 | webapi.RegisterController(&testInjectController{ 47 | BaseController: controller.BaseController{ 48 | Action: map[string]controller.Action{ 49 | "Hello1": {Method: "GET", Params: "ins2"}, 50 | }, 51 | }, 52 | }) 53 | 54 | webapi.RegisterGET("/testInjectMini/testMiniApiInject1", func(str string, val ITestInject) string { 55 | return val.Call() + str 56 | }) 57 | 58 | webapi.RegisterGET("/testInjectMini/testMiniApiInject2", func(str string, val ITestInject) string { 59 | return val.Call() + str 60 | }, "str", "ins2") 61 | 62 | go webapi.Run(":8082") 63 | time.Sleep(10 * time.Millisecond) 64 | 65 | // 测试MVC模式 66 | t.Run("/testinject/hello1", func(t *testing.T) { 67 | rsp, _ := http.Get("http://127.0.0.1:8082/testinject/hello1") 68 | body, _ := io.ReadAll(rsp.Body) 69 | _ = rsp.Body.Close() 70 | assert.Equal(t, "ok2", string(body)) 71 | assert.Equal(t, 200, rsp.StatusCode) 72 | }) 73 | 74 | // 测试mini模式 75 | t.Run("/testInjectMini/testMiniApiInject1", func(t *testing.T) { 76 | rsp, _ := http.Get("http://127.0.0.1:8082/testInjectMini/testMiniApiInject1?str=baby") 77 | body, _ := io.ReadAll(rsp.Body) 78 | _ = rsp.Body.Close() 79 | assert.Equal(t, "ok1baby", string(body)) 80 | assert.Equal(t, 200, rsp.StatusCode) 81 | }) 82 | 83 | // 测试mini模式 84 | t.Run("/testInjectMini/testMiniApiInject2", func(t *testing.T) { 85 | rsp, _ := http.Get("http://127.0.0.1:8082/testInjectMini/testMiniApiInject2?str=oldbaby") 86 | body, _ := io.ReadAll(rsp.Body) 87 | _ = rsp.Body.Close() 88 | assert.Equal(t, "ok2oldbaby", string(body)) 89 | assert.Equal(t, 200, rsp.StatusCode) 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /test/jwt_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "crypto/tls" 5 | "io" 6 | "net/http" 7 | "testing" 8 | "time" 9 | 10 | "github.com/farseer-go/fs" 11 | "github.com/farseer-go/fs/configure" 12 | "github.com/farseer-go/webapi" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func TestJwt(t *testing.T) { 17 | // 测试生成出来的token与head是否一致 18 | var buildToken string 19 | configure.SetDefault("WebApi.Jwt.Key", "123456888") 20 | configure.SetDefault("WebApi.Jwt.KeyType", "HS256") 21 | configure.SetDefault("WebApi.Jwt.HeaderName", "Auto_test") 22 | configure.SetDefault("WebApi.Jwt.InvalidStatusCode", 403) 23 | configure.SetDefault("WebApi.Jwt.InvalidMessage", "您没有权限访问") 24 | 25 | fs.Initialize[webapi.Module]("demo") 26 | 27 | // 颁发Token给到前端 28 | webapi.RegisterRoutes(webapi.Route{Url: "/jwt/build", Action: func() { 29 | claims := make(map[string]any) 30 | claims["farseer-go"] = "v0.8.0" 31 | buildToken, _ = webapi.GetHttpContext().Jwt.Build(claims) // 会写到http head中 32 | }}.POST()) 33 | 34 | webapi.RegisterRoutes(webapi.Route{Url: "/jwt/validate", Action: func() string { 35 | return "hello" 36 | }}.POST().UseJwt()) 37 | 38 | go webapi.Run(":8090") 39 | time.Sleep(10 * time.Millisecond) 40 | 41 | t.Run("test jwt build", func(t *testing.T) { 42 | rsp, _ := http.Post("http://127.0.0.1:8090/jwt/build", "application/json", nil) 43 | _ = rsp.Body.Close() 44 | token := rsp.Header.Get("Auto_test") 45 | assert.Equal(t, token, buildToken) 46 | }) 47 | 48 | t.Run("test jwt validate error", func(t *testing.T) { 49 | newRequest, _ := http.NewRequest("POST", "http://127.0.0.1:8090/jwt/validate", nil) 50 | newRequest.Header.Set("Auto_test", "123123123") 51 | client := &http.Client{ 52 | Transport: &http.Transport{ 53 | TLSClientConfig: &tls.Config{ 54 | InsecureSkipVerify: true, // 不验证 HTTPS 证书 55 | }, 56 | }, 57 | } 58 | rsp, _ := client.Do(newRequest) 59 | rspBytes, _ := io.ReadAll(rsp.Body) 60 | assert.Equal(t, configure.GetString("WebApi.Jwt.InvalidMessage"), string(rspBytes)) 61 | assert.Equal(t, configure.GetInt("WebApi.Jwt.InvalidStatusCode"), rsp.StatusCode) 62 | }) 63 | 64 | t.Run("test jwt validate success", func(t *testing.T) { 65 | newRequest, _ := http.NewRequest("POST", "http://127.0.0.1:8090/jwt/validate", nil) 66 | newRequest.Header.Set("Auto_test", buildToken) 67 | client := &http.Client{ 68 | Transport: &http.Transport{ 69 | TLSClientConfig: &tls.Config{ 70 | InsecureSkipVerify: true, // 不验证 HTTPS 证书 71 | }, 72 | }, 73 | } 74 | rsp, _ := client.Do(newRequest) 75 | rspBytes, _ := io.ReadAll(rsp.Body) 76 | assert.Equal(t, "hello", string(rspBytes)) 77 | assert.Equal(t, 200, rsp.StatusCode) 78 | }) 79 | } 80 | -------------------------------------------------------------------------------- /controller/handleMiddleware.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/farseer-go/fs/container" 5 | "github.com/farseer-go/fs/parse" 6 | "github.com/farseer-go/fs/trace" 7 | "github.com/farseer-go/webapi/context" 8 | "reflect" 9 | ) 10 | 11 | type HandleMiddleware struct { 12 | } 13 | 14 | func (receiver HandleMiddleware) Invoke(httpContext *context.HttpContext) { 15 | traceDetail := container.Resolve[trace.IManager]().TraceHand("执行路由") 16 | defer traceDetail.End(nil) 17 | 18 | // 实例化控制器 19 | controllerVal := reflect.New(httpContext.Route.Controller) 20 | 21 | // 初始化赋控制器 22 | receiver.initController(httpContext, controllerVal) 23 | 24 | // 自动绑定头部 25 | receiver.bindHeader(httpContext, controllerVal) 26 | 27 | actionMethod := controllerVal.MethodByName(httpContext.Route.ActionName) 28 | 29 | var callValues []reflect.Value 30 | // 是否要执行ActionFilter 31 | if httpContext.Route.IsImplActionFilter { 32 | actionFilter := controllerVal.Interface().(IActionFilter) 33 | actionFilter.OnActionExecuting() 34 | 35 | // 实现了check.ICheck(必须放在过滤器之后执行) 36 | httpContext.RequestParamCheck() 37 | callValues = actionMethod.Call(httpContext.Request.Params) // 调用action 38 | 39 | actionFilter.OnActionExecuted() 40 | } else { 41 | // 实现了check.ICheck(必须放在过滤器之后执行) 42 | httpContext.RequestParamCheck() 43 | callValues = actionMethod.Call(httpContext.Request.Params) // 调用action 44 | } 45 | 46 | httpContext.Response.SetValues(callValues...) 47 | } 48 | 49 | // 找到 "controller.BaseController" 字段,并初始化赋值 50 | func (receiver HandleMiddleware) initController(httpContext *context.HttpContext, controllerVal reflect.Value) { 51 | controllerElem := controllerVal.Elem() 52 | for i := 0; i < controllerElem.NumField(); i++ { 53 | fieldVal := controllerElem.Field(i) 54 | if fieldVal.Type().String() == "controller.BaseController" { 55 | fieldVal.Set(reflect.ValueOf(BaseController{HttpContext: httpContext})) 56 | return 57 | } 58 | } 59 | } 60 | 61 | // 绑定header 62 | func (receiver HandleMiddleware) bindHeader(httpContext *context.HttpContext, controllerVal reflect.Value) { 63 | // 没有设置绑定字段,则不需要绑定 64 | if httpContext.Route.AutoBindHeaderName == "" { 65 | return 66 | } 67 | 68 | controllerHeaderVal := controllerVal.Elem().FieldByName(httpContext.Route.AutoBindHeaderName) 69 | controllerHeaderType := controllerHeaderVal.Type() 70 | 71 | // 遍历需要将header绑定的结构体 72 | for headerIndex := 0; headerIndex < controllerHeaderVal.NumField(); headerIndex++ { 73 | headerFieldVal := controllerHeaderVal.Field(headerIndex) 74 | headerFieldType := headerFieldVal.Type() 75 | headerName := controllerHeaderType.Field(headerIndex).Tag.Get("webapi") 76 | if headerName == "" { 77 | headerName = controllerHeaderType.Field(headerIndex).Name 78 | } 79 | headerVal := httpContext.Header.GetValue(headerName) 80 | if headerVal == "" { 81 | continue 82 | } 83 | headerValue := parse.ConvertValue(headerVal, headerFieldType) 84 | headerFieldVal.Set(reflect.ValueOf(headerValue)) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /session-redis/context/httpSession.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "github.com/farseer-go/cache" 5 | "github.com/farseer-go/cacheMemory" 6 | "github.com/farseer-go/fs/configure" 7 | "github.com/farseer-go/fs/container" 8 | "github.com/farseer-go/fs/sonyflake" 9 | "github.com/farseer-go/redis" 10 | "net/http" 11 | "strconv" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | // session名称 17 | const sessionId = "SessionId" 18 | 19 | // SessionTimeout 自动过期时间,单位:秒 20 | var SessionTimeout = 1200 21 | 22 | // SessionEnable 开启Session 23 | var SessionEnable = false 24 | 25 | // 存储session每一项值 26 | type nameValue struct { 27 | Name string 28 | Value any 29 | } 30 | 31 | type HttpSession struct { 32 | id string 33 | store cache.ICacheManage[nameValue] 34 | } 35 | 36 | // InitSession 初始化httpSession 37 | func InitSession(w http.ResponseWriter, r *http.Request) *HttpSession { 38 | httpSession := &HttpSession{} 39 | 40 | c, _ := r.Cookie(sessionId) 41 | if c != nil { 42 | httpSession.id = c.Value 43 | } 44 | 45 | // 第一次请求 46 | if httpSession.id == "" { 47 | httpSession.id = strconv.FormatInt(sonyflake.GenerateId(), 10) 48 | // 写入Cookies 49 | http.SetCookie(w, &http.Cookie{ 50 | Name: sessionId, 51 | Value: httpSession.id, 52 | Path: "/", 53 | HttpOnly: true, 54 | }) 55 | } 56 | 57 | // 设置存储方式 58 | cacheId := "SessionId_" + httpSession.id 59 | if !container.IsRegister[cache.ICacheManage[nameValue]](cacheId) { 60 | ops := func(op *cache.Op) { 61 | op.SlidingExpiration(time.Duration(SessionTimeout) * time.Second) 62 | } 63 | // 根据配置,设置存储方式 64 | switch strings.ToLower(configure.GetString("Webapi.Session.Store")) { 65 | case "redis": 66 | httpSession.store = redis.SetProfiles[nameValue](cacheId, "Name", configure.GetString("Webapi.Session.StoreConfigName"), ops) 67 | default: 68 | httpSession.store = cacheMemory.SetProfiles[nameValue](cacheId, "Name", ops) 69 | } 70 | } else { 71 | httpSession.store = container.Resolve[cache.ICacheManage[nameValue]](cacheId) 72 | } 73 | return httpSession 74 | } 75 | 76 | // GetValue 获取Session 77 | func (r *HttpSession) GetValue(name string) any { 78 | item, _ := r.store.GetItem(name) 79 | return item.Value 80 | } 81 | 82 | // SetValue 设置Session 83 | func (r *HttpSession) SetValue(name string, val any) { 84 | r.store.SaveItem(nameValue{ 85 | Name: name, 86 | Value: val, 87 | }) 88 | } 89 | 90 | // Remove 删除Session 91 | func (r *HttpSession) Remove(name string) { 92 | r.store.Remove(name) 93 | } 94 | 95 | // Clear 清空Session 96 | func (r *HttpSession) Clear() { 97 | r.store.Clear() 98 | } 99 | 100 | // ClearSession 移除过期的Session对象 101 | func ClearSession() { 102 | if !SessionEnable { 103 | SessionEnable = true 104 | tick := time.NewTicker(60 * time.Second) 105 | for range tick.C { 106 | container.RemoveUnused[cache.ICacheManage[nameValue]](time.Duration(SessionTimeout) * time.Second) 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /context/httpResponse.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "net/http" 5 | "reflect" 6 | 7 | "github.com/farseer-go/fs/snc" 8 | ) 9 | 10 | type HttpResponse struct { 11 | W http.ResponseWriter 12 | Body []any // Action执行的结果(Action返回值) 13 | BodyBytes []byte // 自定义输出结果 14 | httpCode int // http响应代码 15 | statusCode int // ApiResponse响应代码 16 | statusMessage string // ApiResponse响应提示 17 | } 18 | 19 | // GetHttpCode 获取响应的HttpCode 20 | func (receiver *HttpResponse) GetHttpCode() int { 21 | return receiver.httpCode 22 | } 23 | 24 | // SetHttpCode 将响应状态写入http流 25 | func (receiver *HttpResponse) SetHttpCode(httpCode int) { 26 | receiver.httpCode = httpCode 27 | } 28 | 29 | // Write 将响应内容写入http流 30 | func (receiver *HttpResponse) Write(content []byte) { 31 | receiver.BodyBytes = content 32 | } 33 | 34 | // WriteString 将响应内容写入http流 35 | func (receiver *HttpResponse) WriteString(content string) { 36 | receiver.BodyBytes = []byte(content) 37 | } 38 | 39 | // WriteJson 将响应内容转成json后写入http流 40 | func (receiver *HttpResponse) WriteJson(content any) { 41 | receiver.BodyBytes, _ = snc.Marshal(content) 42 | receiver.W.Header().Set("Content-Type", "application/json") 43 | } 44 | 45 | // AddHeader 添加头部 46 | func (receiver *HttpResponse) AddHeader(key, value string) { 47 | receiver.W.Header().Add(key, value) 48 | } 49 | 50 | // SetHeader 覆盖头部 51 | func (receiver *HttpResponse) SetHeader(key, value string) { 52 | receiver.W.Header().Set(key, value) 53 | } 54 | 55 | // DelHeader 删除头部 56 | func (receiver *HttpResponse) DelHeader(key string) { 57 | receiver.W.Header().Del(key) 58 | } 59 | 60 | // SetStatusCode 设置StatusCode 61 | func (receiver *HttpResponse) SetStatusCode(statusCode int) { 62 | receiver.statusCode = statusCode 63 | } 64 | 65 | // SetMessage 设计响应提示信息 66 | func (receiver *HttpResponse) SetMessage(statusCode int, statusMessage string) { 67 | receiver.statusCode = statusCode 68 | receiver.statusMessage = statusMessage 69 | } 70 | 71 | // Reject 拒绝服务 72 | func (receiver *HttpResponse) Reject(httpCode int, content string) { 73 | receiver.httpCode = httpCode 74 | receiver.statusCode = httpCode 75 | receiver.BodyBytes = []byte(content) 76 | } 77 | 78 | // GetStatus 获取statusCode、statusMessage 79 | func (receiver *HttpResponse) GetStatus() (int, string) { 80 | return receiver.statusCode, receiver.statusMessage 81 | } 82 | 83 | // SetValues 设置Body值 84 | func (receiver *HttpResponse) SetValues(callValues ...reflect.Value) { 85 | for _, value := range callValues { 86 | receiver.Body = append(receiver.Body, value.Interface()) 87 | } 88 | } 89 | 90 | // Redirect302 302重定向 91 | func (receiver *HttpResponse) Redirect302(location string) { 92 | receiver.httpCode = 302 93 | receiver.W.Header().Set("Location", location) 94 | } 95 | 96 | // Redirect301 301重定向 97 | func (receiver *HttpResponse) Redirect301(location string) { 98 | receiver.httpCode = 301 99 | receiver.W.Header().Set("Location", location) 100 | } 101 | -------------------------------------------------------------------------------- /run.go: -------------------------------------------------------------------------------- 1 | package webapi 2 | 3 | import ( 4 | "github.com/farseer-go/fs/modules" 5 | "github.com/farseer-go/webapi/context" 6 | "github.com/farseer-go/webapi/controller" 7 | ) 8 | 9 | var defaultApi *applicationBuilder 10 | 11 | func RegisterMiddleware(m context.IMiddleware) { 12 | // 需要先依赖模块 13 | modules.ThrowIfNotLoad(Module{}) 14 | defaultApi.RegisterMiddleware(m) 15 | } 16 | 17 | // RegisterRoutes 批量注册路由 18 | func RegisterRoutes(routes ...Route) { 19 | defaultApi.RegisterRoutes(routes...) 20 | } 21 | 22 | // RegisterController 自动注册控制器下的所有Action方法 23 | func RegisterController(c controller.IController) { 24 | // 需要先依赖模块 25 | modules.ThrowIfNotLoad(Module{}) 26 | defaultApi.RegisterController(c) 27 | } 28 | 29 | // RegisterPOST 注册单个Api(支持占位符,例如:/{cateId}/{Id}) 30 | func RegisterPOST(route string, actionFunc any, params ...string) { 31 | defaultApi.RegisterPOST(route, actionFunc, params...) 32 | } 33 | 34 | // RegisterGET 注册单个Api(支持占位符,例如:/{cateId}/{Id}) 35 | func RegisterGET(route string, actionFunc any, params ...string) { 36 | defaultApi.RegisterGET(route, actionFunc, params...) 37 | } 38 | 39 | // RegisterPUT 注册单个Api(支持占位符,例如:/{cateId}/{Id}) 40 | func RegisterPUT(route string, actionFunc any, params ...string) { 41 | defaultApi.RegisterPUT(route, actionFunc, params...) 42 | } 43 | 44 | // RegisterDELETE 注册单个Api(支持占位符,例如:/{cateId}/{Id}) 45 | func RegisterDELETE(route string, actionFunc any, params ...string) { 46 | defaultApi.RegisterDELETE(route, actionFunc, params...) 47 | } 48 | 49 | // Area 设置区域 50 | func Area(area string, f func()) { 51 | defaultApi.Area(area, f) 52 | } 53 | 54 | // UseCors 使用CORS中间件 55 | func UseCors() { 56 | defaultApi.UseCors() 57 | } 58 | 59 | // UseStaticFiles 支持静态目录,在根目录./wwwroot中的文件,直接以静态文件提供服务 60 | func UseStaticFiles() { 61 | // 需要先依赖模块 62 | modules.ThrowIfNotLoad(Module{}) 63 | 64 | defaultApi.UseStaticFiles() 65 | } 66 | 67 | // UsePprof 是否同时开启pprof 68 | func UsePprof() { 69 | defaultApi.UsePprof() 70 | } 71 | 72 | // UseSession 开启Session 73 | func UseSession() { 74 | defaultApi.UseSession() 75 | } 76 | 77 | func UseWebApi() { 78 | defaultApi.UseWebApi() 79 | } 80 | 81 | // UseApiResponse 让所有的返回值,包含在core.ApiResponse中 82 | func UseApiResponse() { 83 | defaultApi.UseApiResponse() 84 | } 85 | 86 | // UseValidate 使用字段验证器 87 | func UseValidate() { 88 | defaultApi.UseValidate() 89 | } 90 | 91 | // UseTLS 使用https 92 | func UseTLS(certFile, keyFile string) { 93 | defaultApi.UseTLS(certFile, keyFile) 94 | } 95 | 96 | // Run 运行Web服务(默认8888端口) 97 | func Run(params ...string) { 98 | // 需要先依赖模块 99 | modules.ThrowIfNotLoad(Module{}) 100 | defaultApi.Run(params...) 101 | } 102 | 103 | // PrintRoute 打印所有路由信息到控制台 104 | func PrintRoute() { 105 | defaultApi.PrintRoute() 106 | } 107 | 108 | // UseApiDoc 是否开启Api文档 109 | func UseApiDoc() { 110 | defaultApi.UseApiDoc() 111 | } 112 | 113 | // UseHealthCheck 【GET】开启健康检查(默认route = "/healthCheck") 114 | func UseHealthCheck(routes ...string) { 115 | defaultApi.UseHealthCheck(routes...) 116 | } 117 | -------------------------------------------------------------------------------- /test/websocket_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | "time" 7 | 8 | "github.com/farseer-go/fs" 9 | "github.com/farseer-go/fs/core" 10 | "github.com/farseer-go/fs/trace" 11 | "github.com/farseer-go/utils/ws" 12 | "github.com/farseer-go/webapi" 13 | "github.com/farseer-go/webapi/websocket" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | func TestWebsocket(t *testing.T) { 18 | fs.Initialize[webapi.Module]("demo") 19 | webapi.UseStaticFiles() 20 | webapi.UseApiResponse() 21 | webapi.RegisterRoutes(webapi.Route{Url: "/mini/api", Method: "GET", Action: func() any { 22 | return pageSizeRequest{PageSize: 3, PageIndex: 2} 23 | }}) 24 | 25 | // 场景一:客户端发一次消息,服务端返回一次消息 26 | webapi.RegisterRoutes(webapi.Route{Url: "/ws/api1", Method: "WS", Params: []string{"context", ""}, 27 | Action: func(context *websocket.Context[pageSizeRequest], manager trace.IManager) { 28 | // 验证头部 29 | val := context.GetHeader("Token") 30 | assert.Equal(t, "farseer-go", val) 31 | 32 | // 验证a、c 33 | assert.Equal(t, "b", context.HttpContext.Request.Form["a"]) 34 | assert.Equal(t, "d", context.HttpContext.Request.Form["c"]) 35 | 36 | req := context.Receiver() 37 | _ = context.Send("我收到消息啦:") 38 | 39 | req.PageSize++ 40 | req.PageIndex++ 41 | _ = context.Send(req) 42 | }}) 43 | 44 | // 场景二:客户端连接后,客户端每发一次消息,服务端持续返回新的消息 45 | webapi.RegisterRoutes(webapi.Route{Url: "/ws/api2", Method: "WS", Params: []string{"context", ""}, 46 | Action: func(context *websocket.Context[pageSizeRequest], manager trace.IManager) { 47 | context.ReceiverMessageFunc(500*time.Millisecond, func(message string) { 48 | if message == "1" { 49 | _ = context.Send("hello") 50 | } 51 | 52 | if message == "2" { 53 | _ = context.Send("world") 54 | } 55 | }) 56 | }}) 57 | 58 | go webapi.Run(":8096") 59 | time.Sleep(100 * time.Millisecond) 60 | 61 | t.Run("mini/api", func(t *testing.T) { 62 | rsp, _ := http.Get("http://127.0.0.1:8096/mini/api") 63 | apiResponse := core.NewApiResponseByReader[pageSizeRequest](rsp.Body) 64 | _ = rsp.Body.Close() 65 | assert.Equal(t, 3, apiResponse.Data.PageSize) 66 | assert.Equal(t, 2, apiResponse.Data.PageIndex) 67 | assert.Equal(t, 200, rsp.StatusCode) 68 | assert.Equal(t, 200, apiResponse.StatusCode) 69 | }) 70 | 71 | t.Run("/ws/api1", func(t *testing.T) { 72 | client, err := ws.NewClient("ws://127.0.0.1:8096/ws/api1?a=b&c=d", 1024, true) 73 | assert.Nil(t, err) 74 | 75 | // 设置头部 76 | client.SetHeader("token", "farseer-go") 77 | 78 | // 连接 79 | err = client.Connect() 80 | assert.Nil(t, err) 81 | 82 | // 发消息 83 | err = client.Send(pageSizeRequest{ 84 | PageSize: 200, 85 | PageIndex: 100, 86 | }) 87 | assert.Nil(t, err) 88 | 89 | // 接收服务端的消息 90 | msg, err := client.ReceiverMessage() 91 | assert.Nil(t, err) 92 | assert.Equal(t, msg, "我收到消息啦:") 93 | 94 | // 接收服务端的消息 95 | var request2 pageSizeRequest 96 | err = client.Receiver(&request2) 97 | assert.Nil(t, err) 98 | assert.Equal(t, 201, request2.PageSize) 99 | assert.Equal(t, 101, request2.PageIndex) 100 | 101 | time.Sleep(100 * time.Millisecond) 102 | // 服务端关闭后,尝试继续接收消息 103 | assert.Panics(t, func() { 104 | _, _ = client.ReceiverMessage() 105 | }) 106 | }) 107 | 108 | t.Run("/ws/api2", func(t *testing.T) { 109 | client, err := ws.NewClient("ws://127.0.0.1:8096/ws/api2", 1024, true) 110 | assert.Nil(t, err) 111 | 112 | // 连接 113 | err = client.Connect() 114 | assert.Nil(t, err) 115 | 116 | // 发送1 117 | _ = client.Send("1") 118 | 119 | msg, _ := client.ReceiverMessage() 120 | assert.Equal(t, msg, "hello") 121 | 122 | msg, _ = client.ReceiverMessage() 123 | assert.Equal(t, msg, "hello") 124 | 125 | _ = client.Send("2") 126 | 127 | msg, _ = client.ReceiverMessage() 128 | assert.Equal(t, msg, "world") 129 | 130 | msg, _ = client.ReceiverMessage() 131 | assert.Equal(t, msg, "world") 132 | }) 133 | } 134 | -------------------------------------------------------------------------------- /test/actionResult_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/farseer-go/fs" 5 | "github.com/farseer-go/fs/configure" 6 | "github.com/farseer-go/fs/core" 7 | "github.com/farseer-go/webapi" 8 | "github.com/farseer-go/webapi/action" 9 | "github.com/stretchr/testify/assert" 10 | "io" 11 | "net/http" 12 | "net/url" 13 | "strconv" 14 | "testing" 15 | "time" 16 | ) 17 | 18 | func TestActionResult(t *testing.T) { 19 | fs.Initialize[webapi.Module]("demo") 20 | configure.SetDefault("Log.Component.webapi", true) 21 | webapi.RegisterGET("/redirect", func() any { 22 | return pageSizeRequest{PageSize: 3, PageIndex: 2} 23 | }) 24 | 25 | webapi.RegisterPOST("/mini/testActionResult", func(actionType int) action.IResult { 26 | switch actionType { 27 | case 0: 28 | return action.Redirect("/redirect") 29 | case 1: 30 | return action.View("") 31 | case 2: 32 | return action.View("testActionResult") 33 | case 3: 34 | return action.View("test.txt") 35 | case 4: 36 | return action.Content("ccc") 37 | case 5: 38 | return action.FileContent("./views/mini/test.log") 39 | } 40 | 41 | return action.Content("eee") 42 | }) 43 | webapi.UseApiResponse() 44 | 45 | go webapi.Run(":8088") 46 | time.Sleep(10 * time.Millisecond) 47 | 48 | t.Run("test-0", func(t *testing.T) { 49 | val := make(url.Values) 50 | val.Add("actionType", strconv.Itoa(0)) 51 | rsp, _ := http.PostForm("http://127.0.0.1:8088/mini/testActionResult", val) 52 | apiResponse := core.NewApiResponseByReader[pageSizeRequest](rsp.Body) 53 | _ = rsp.Body.Close() 54 | assert.Equal(t, 3, apiResponse.Data.PageSize) 55 | assert.Equal(t, 2, apiResponse.Data.PageIndex) 56 | assert.Equal(t, 200, rsp.StatusCode) 57 | assert.Equal(t, 200, apiResponse.StatusCode) 58 | }) 59 | 60 | t.Run("test-1", func(t *testing.T) { 61 | val := make(url.Values) 62 | val.Add("actionType", strconv.Itoa(1)) 63 | rsp, _ := http.PostForm("http://127.0.0.1:8088/mini/testActionResult", val) 64 | body, _ := io.ReadAll(rsp.Body) 65 | _ = rsp.Body.Close() 66 | assert.Equal(t, "aaa", string(body)) 67 | assert.Equal(t, 200, rsp.StatusCode) 68 | }) 69 | 70 | t.Run("test-2", func(t *testing.T) { 71 | val := make(url.Values) 72 | val.Add("actionType", strconv.Itoa(2)) 73 | rsp, _ := http.PostForm("http://127.0.0.1:8088/mini/testActionResult", val) 74 | body, _ := io.ReadAll(rsp.Body) 75 | _ = rsp.Body.Close() 76 | assert.Equal(t, "aaa", string(body)) 77 | assert.Equal(t, 200, rsp.StatusCode) 78 | }) 79 | 80 | t.Run("test-3", func(t *testing.T) { 81 | val := make(url.Values) 82 | val.Add("actionType", strconv.Itoa(3)) 83 | rsp, _ := http.PostForm("http://127.0.0.1:8088/mini/testActionResult", val) 84 | body, _ := io.ReadAll(rsp.Body) 85 | _ = rsp.Body.Close() 86 | assert.Equal(t, "bbb", string(body)) 87 | assert.Equal(t, 200, rsp.StatusCode) 88 | }) 89 | 90 | t.Run("test-4", func(t *testing.T) { 91 | val := make(url.Values) 92 | val.Add("actionType", strconv.Itoa(4)) 93 | rsp, _ := http.PostForm("http://127.0.0.1:8088/mini/testActionResult", val) 94 | body, _ := io.ReadAll(rsp.Body) 95 | _ = rsp.Body.Close() 96 | assert.Equal(t, "ccc", string(body)) 97 | assert.Equal(t, 200, rsp.StatusCode) 98 | }) 99 | 100 | t.Run("test-5", func(t *testing.T) { 101 | val := make(url.Values) 102 | val.Add("actionType", strconv.Itoa(5)) 103 | rsp, _ := http.PostForm("http://127.0.0.1:8088/mini/testActionResult", val) 104 | body, _ := io.ReadAll(rsp.Body) 105 | _ = rsp.Body.Close() 106 | assert.Equal(t, "ddd", string(body)) 107 | assert.Equal(t, 200, rsp.StatusCode) 108 | }) 109 | 110 | t.Run("test--1", func(t *testing.T) { 111 | val := make(url.Values) 112 | val.Add("actionType", strconv.Itoa(-1)) 113 | rsp, _ := http.PostForm("http://127.0.0.1:8088/mini/testActionResult", val) 114 | body, _ := io.ReadAll(rsp.Body) 115 | _ = rsp.Body.Close() 116 | assert.Equal(t, "eee", string(body)) 117 | assert.Equal(t, 200, rsp.StatusCode) 118 | }) 119 | } 120 | -------------------------------------------------------------------------------- /apiDoc.go: -------------------------------------------------------------------------------- 1 | package webapi 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | 8 | "github.com/farseer-go/collections" 9 | "github.com/farseer-go/fs/core" 10 | "github.com/farseer-go/fs/fastReflect" 11 | "github.com/farseer-go/fs/parse" 12 | "github.com/farseer-go/fs/snc" 13 | "github.com/farseer-go/fs/types" 14 | "github.com/farseer-go/webapi/action" 15 | "github.com/farseer-go/webapi/context" 16 | ) 17 | 18 | // UseApiDoc 是否开启Api文档 19 | func (r *applicationBuilder) UseApiDoc() { 20 | r.registerAction(Route{Url: "/doc/api", Method: "GET", Action: func() action.IResult { 21 | lstBody := collections.NewList[string]("
\n") 22 | lstRoute := collections.NewList[context.HttpRoute]() 23 | for _, httpRoute := range r.mux.m { 24 | lstRoute.Add(*httpRoute) 25 | } 26 | 27 | // 遍历路由 28 | lstRoute.OrderBy(func(item context.HttpRoute) any { 29 | return item.RouteUrl 30 | }).Foreach(func(httpRoute *context.HttpRoute) { 31 | method := strings.Join(httpRoute.Method.ToArray(), "|") 32 | if httpRoute.RouteUrl == "/" && method == "" && httpRoute.Controller == nil && httpRoute.Action == nil { 33 | return 34 | } 35 | // API地址 36 | lstBody.Add(fmt.Sprintf("[%s]:%s