diff --git a/net/ghttp/ghttp.go b/net/ghttp/ghttp.go index b9354f3f9..bb8c14f03 100644 --- a/net/ghttp/ghttp.go +++ b/net/ghttp/ghttp.go @@ -12,6 +12,7 @@ import ( "github.com/gogf/gf/container/gtype" "github.com/gogf/gf/os/gcache" "github.com/gogf/gf/os/gsession" + "github.com/gogf/gf/protocol/goai" "github.com/gorilla/websocket" "net/http" "reflect" @@ -32,6 +33,7 @@ type ( routesMap map[string][]registeredRouteItem // Route map mainly for route dumps and repeated route checks. statusHandlerMap map[string][]HandlerFunc // Custom status handler map. sessionManager *gsession.Manager // Session manager. + openapi *goai.OpenApiV3 // The OpenApi specification management object. } // Router object. @@ -46,6 +48,7 @@ type ( // RouterItem is just for route dumps. RouterItem struct { + Handler *handlerItem // The handler. Server string // Server name. Address string // Listening address. Domain string // Bound domain. @@ -55,13 +58,12 @@ type ( Route string // Route URI. Priority int // Just for reference. IsServiceHandler bool // Is service handler. - handler *handlerItem // The handler. } // HandlerFunc is request handler function. HandlerFunc = func(r *Request) - // handlerFuncInfo contains the HandlerFunc address and its reflect type. + // handlerFuncInfo contains the HandlerFunc address and its reflection type. handlerFuncInfo struct { Func HandlerFunc // Handler function address. Type reflect.Type // Reflect type information for current handler, which is used for extension of handler feature. diff --git a/net/ghttp/ghttp_middleware_handler_response.go b/net/ghttp/ghttp_middleware_handler_response.go index e37e92374..32b246b79 100644 --- a/net/ghttp/ghttp_middleware_handler_response.go +++ b/net/ghttp/ghttp_middleware_handler_response.go @@ -21,6 +21,12 @@ type DefaultHandlerResponse struct { // MiddlewareHandlerResponse is the default middleware handling handler response object and its error. func MiddlewareHandlerResponse(r *Request) { r.Middleware.Next() + + // There's custom buffer content, it then exits current handler. + if r.Response.BufferLength() > 0 { + return + } + var ( err error res interface{} diff --git a/net/ghttp/ghttp_server.go b/net/ghttp/ghttp_server.go index 379449285..dcdb19dc7 100644 --- a/net/ghttp/ghttp_server.go +++ b/net/ghttp/ghttp_server.go @@ -13,6 +13,7 @@ import ( "github.com/gogf/gf/errors/gcode" "github.com/gogf/gf/errors/gerror" "github.com/gogf/gf/internal/intlog" + "github.com/gogf/gf/protocol/goai" "net/http" "os" "runtime" @@ -99,6 +100,7 @@ func GetServer(name ...interface{}) *Server { serveTree: make(map[string]interface{}), serveCache: gcache.New(), routesMap: make(map[string][]registeredRouteItem), + openapi: goai.New(), } // Initialize the server using default configurations. if err := s.SetConfig(NewConfig()); err != nil { @@ -116,6 +118,11 @@ func (s *Server) Start() error { ctx = context.TODO() ) + // OpenApi specification json producing handler. + if s.config.OpenApiPath != "" { + s.BindHandler(s.config.OpenApiPath, s.openapiSpecJson) + } + // Register group routes. s.handlePreBindItems(ctx) @@ -202,6 +209,7 @@ func (s *Server) Start() error { } }) } + s.initOpenApi() s.dumpRouterMap() return nil } @@ -219,14 +227,14 @@ func (s *Server) dumpRouterMap() { table.SetBorder(false) table.SetCenterSeparator("|") - for _, item := range s.GetRouterArray() { + for _, item := range s.GetRoutes() { data := make([]string, 7) data[0] = item.Server data[1] = item.Domain data[2] = item.Address data[3] = item.Method data[4] = item.Route - data[5] = item.handler.Name + data[5] = item.Handler.Name data[6] = item.Middleware table.Append(data) } @@ -235,9 +243,14 @@ func (s *Server) dumpRouterMap() { } } -// GetRouterArray retrieves and returns the router array. +// GetOpenApi returns the OpenApi specification management object of current server. +func (s *Server) GetOpenApi() *goai.OpenApiV3 { + return s.openapi +} + +// GetRoutes retrieves and returns the router array. // The key of the returned map is the domain of the server. -func (s *Server) GetRouterArray() []RouterItem { +func (s *Server) GetRoutes() []RouterItem { m := make(map[string]*garray.SortedArray) address := s.config.Address if s.config.HTTPSAddr != "" { @@ -258,16 +271,17 @@ func (s *Server) GetRouterArray() []RouterItem { Method: array[2], Route: array[3], Priority: len(registeredItems) - index - 1, - handler: registeredItem.Handler, + Handler: registeredItem.Handler, } - switch item.handler.Type { + switch item.Handler.Type { case handlerTypeController, handlerTypeObject, handlerTypeHandler: item.IsServiceHandler = true + case handlerTypeMiddleware: item.Middleware = "GLOBAL MIDDLEWARE" } - if len(item.handler.Middleware) > 0 { - for _, v := range item.handler.Middleware { + if len(item.Handler.Middleware) > 0 { + for _, v := range item.Handler.Middleware { if item.Middleware != "" { item.Middleware += "," } @@ -285,9 +299,9 @@ func (s *Server) GetRouterArray() []RouterItem { if r = strings.Compare(item1.Domain, item2.Domain); r == 0 { if r = strings.Compare(item1.Route, item2.Route); r == 0 { if r = strings.Compare(item1.Method, item2.Method); r == 0 { - if item1.handler.Type == handlerTypeMiddleware && item2.handler.Type != handlerTypeMiddleware { + if item1.Handler.Type == handlerTypeMiddleware && item2.Handler.Type != handlerTypeMiddleware { return -1 - } else if item1.handler.Type == handlerTypeMiddleware && item2.handler.Type == handlerTypeMiddleware { + } else if item1.Handler.Type == handlerTypeMiddleware && item2.Handler.Type == handlerTypeMiddleware { return 1 } else if r = strings.Compare(item1.Middleware, item2.Middleware); r == 0 { r = item2.Priority - item1.Priority diff --git a/net/ghttp/ghttp_server_config.go b/net/ghttp/ghttp_server_config.go index fb27bc172..c402e3a84 100644 --- a/net/ghttp/ghttp_server_config.go +++ b/net/ghttp/ghttp_server_config.go @@ -166,7 +166,7 @@ type ServerConfig struct { SessionStorage gsession.Storage `json:"sessionStorage"` // SessionCookieMaxAge specifies the cookie ttl for session id. - // It it is set 0, it means it expires along with browser session. + // If it is set 0, it means it expires along with browser session. SessionCookieMaxAge time.Duration `json:"sessionCookieMaxAge"` // SessionCookieOutput specifies whether automatic outputting session id to cookie. @@ -197,7 +197,7 @@ type ServerConfig struct { // ClientMaxBodySize specifies the max body size limit in bytes for client request. // It can be configured in configuration file using string like: 1m, 10m, 500kb etc. - // It's 8MB in default. + // It's `8MB` in default. ClientMaxBodySize int64 `json:"clientMaxBodySize"` // FormParsingMemory specifies max memory buffer size in bytes which can be used for @@ -221,6 +221,9 @@ type ServerConfig struct { // GracefulTimeout set the maximum survival time (seconds) of the parent process. GracefulTimeout uint8 `json:"gracefulTimeout"` + + // OpenApiPath specifies the OpenApi specification file path. + OpenApiPath string `json:"openapiPath"` } // NewConfig creates and returns a ServerConfig object with default configurations. @@ -238,7 +241,7 @@ func NewConfig() ServerConfig { KeepAlive: true, IndexFiles: []string{"index.html", "index.htm"}, IndexFolder: false, - ServerAgent: "GF HTTP Server", + ServerAgent: "GoFrame HTTP Server", ServerRoot: "", StaticPaths: make([]staticPathItem, 0), FileServerEnabled: false, @@ -264,6 +267,7 @@ func NewConfig() ServerConfig { Rewrites: make(map[string]string), Graceful: false, GracefulTimeout: 2, // seconds + OpenApiPath: `/api.json`, } } diff --git a/net/ghttp/ghttp_server_openapi.go b/net/ghttp/ghttp_server_openapi.go new file mode 100644 index 000000000..019013c75 --- /dev/null +++ b/net/ghttp/ghttp_server_openapi.go @@ -0,0 +1,43 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package ghttp + +import ( + "github.com/gogf/gf/internal/intlog" + "github.com/gogf/gf/protocol/goai" + "github.com/gogf/gf/text/gstr" +) + +func (s *Server) initOpenApi() { + var ( + err error + method string + ) + for _, item := range s.GetRoutes() { + method = item.Method + if gstr.Equal(method, defaultMethod) { + method = "POST" + } + if item.Handler.Info.Func == nil { + err = s.openapi.Add(goai.AddInput{ + Path: item.Route, + Method: method, + Object: item.Handler.Info.Value.Interface(), + }) + if err != nil { + panic(err) + } + } + } +} + +func (s *Server) openapiSpecJson(r *Request) { + err := r.Response.WriteJson(s.openapi) + if err != nil { + intlog.Error(r.Context(), err) + } +} diff --git a/net/ghttp/ghttp_server_router_group.go b/net/ghttp/ghttp_server_router_group.go index 39e8661a9..68c644887 100644 --- a/net/ghttp/ghttp_server_router_group.go +++ b/net/ghttp/ghttp_server_router_group.go @@ -334,7 +334,7 @@ func (g *RouterGroup) doBindRoutersToServer(ctx context.Context, item *preBindIt if reflect.ValueOf(object).Kind() == reflect.Func { funcInfo, err := g.server.checkAndCreateFuncInfo(object, "", "", "") if err != nil { - g.server.Logger().Error(ctx, err.Error()) + g.server.Logger().Fatal(ctx, err.Error()) return g } if g.server != nil { diff --git a/net/ghttp/ghttp_server_service_handler.go b/net/ghttp/ghttp_server_service_handler.go index bc40bf626..c8f8c785a 100644 --- a/net/ghttp/ghttp_server_service_handler.go +++ b/net/ghttp/ghttp_server_service_handler.go @@ -31,8 +31,7 @@ func (s *Server) BindHandler(pattern string, handler interface{}) { ) funcInfo, err := s.checkAndCreateFuncInfo(handler, "", "", "") if err != nil { - s.Logger().Error(ctx, err.Error()) - return + s.Logger().Fatal(ctx, err) } s.doBindHandler(ctx, pattern, funcInfo, nil, "") } @@ -131,17 +130,17 @@ func (s *Server) checkAndCreateFuncInfo(f interface{}, pkgPath, structName, meth handlerFunc, ok := f.(HandlerFunc) if !ok { reflectType := reflect.TypeOf(f) - if reflectType.NumIn() == 0 || reflectType.NumIn() > 2 || reflectType.NumOut() > 2 { + if reflectType.NumIn() != 2 || reflectType.NumOut() != 2 { if pkgPath != "" { err = gerror.NewCodef( gcode.CodeInvalidParameter, - `invalid handler: %s.%s.%s defined as "%s", but "func(*ghttp.Request)" or "func(context.Context)/func(context.Context,Request)/func(context.Context,Request) error/func(context.Context,Request)(Response,error)" is required`, + `invalid handler: %s.%s.%s defined as "%s", but "func(*ghttp.Request)" or "func(context.Context, Request)(Response, error)" is required`, pkgPath, structName, methodName, reflect.TypeOf(f).String(), ) } else { err = gerror.NewCodef( gcode.CodeInvalidParameter, - `invalid handler: defined as "%s", but "func(*ghttp.Request)" or "func(context.Context)/func(context.Context,Request)/func(context.Context,Request) error/func(context.Context,Request)(Response,error)" is required`, + `invalid handler: defined as "%s", but "func(*ghttp.Request)" or "func(context.Context, Request)(Response, error)" is required`, reflect.TypeOf(f).String(), ) } @@ -157,7 +156,7 @@ func (s *Server) checkAndCreateFuncInfo(f interface{}, pkgPath, structName, meth return } - if reflectType.NumOut() > 0 && reflectType.Out(reflectType.NumOut()-1).String() != "error" { + if reflectType.Out(1).String() != "error" { err = gerror.NewCodef( gcode.CodeInvalidParameter, `invalid handler: defined as "%s", but the last output parameter should be type of "error"`, diff --git a/net/ghttp/ghttp_server_service_object.go b/net/ghttp/ghttp_server_service_object.go index 397432585..eab9a18ab 100644 --- a/net/ghttp/ghttp_server_service_object.go +++ b/net/ghttp/ghttp_server_service_object.go @@ -109,8 +109,7 @@ func (s *Server) doBindObject(ctx context.Context, pattern string, object interf funcInfo, err := s.checkAndCreateFuncInfo(v.Method(i).Interface(), pkgPath, objName, methodName) if err != nil { - s.Logger().Error(ctx, err.Error()) - return + s.Logger().Fatal(ctx, err) } key := s.mergeBuildInNameToPattern(pattern, structName, methodName, true) @@ -188,8 +187,7 @@ func (s *Server) doBindObjectMethod(ctx context.Context, pattern string, object funcInfo, err := s.checkAndCreateFuncInfo(methodValue.Interface(), pkgPath, objName, methodName) if err != nil { - s.Logger().Error(ctx, err.Error()) - return + s.Logger().Fatal(ctx, err) } key := s.mergeBuildInNameToPattern(pattern, structName, methodName, false) @@ -243,8 +241,7 @@ func (s *Server) doBindObjectRest(ctx context.Context, pattern string, object in funcInfo, err := s.checkAndCreateFuncInfo(v.Method(i).Interface(), pkgPath, objName, methodName) if err != nil { - s.Logger().Error(ctx, err.Error()) - return + s.Logger().Fatal(ctx, err) } key := s.mergeBuildInNameToPattern(methodName+":"+pattern, structName, methodName, false) diff --git a/net/ghttp/ghttp_unit_router_handler_extended_test.go b/net/ghttp/ghttp_unit_router_handler_extended_test.go index c89aa21af..d8e19b408 100644 --- a/net/ghttp/ghttp_unit_router_handler_extended_test.go +++ b/net/ghttp/ghttp_unit_router_handler_extended_test.go @@ -18,27 +18,6 @@ import ( "github.com/gogf/gf/test/gtest" ) -func Test_Router_Handler_Extended_Handler_Basic(t *testing.T) { - p, _ := ports.PopRand() - s := g.Server(p) - s.BindHandler("/test", func(ctx context.Context) { - r := g.RequestFromCtx(ctx) - r.Response.Write("test") - }) - s.SetPort(p) - s.SetDumpRouterMap(false) - s.Start() - defer s.Shutdown() - - time.Sleep(100 * time.Millisecond) - gtest.C(t, func(t *gtest.T) { - client := g.Client() - client.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", p)) - - t.Assert(client.GetContent(ctx, "/test"), "test") - }) -} - func Test_Router_Handler_Extended_Handler_WithObject(t *testing.T) { type TestReq struct { Age int diff --git a/protocol/goai/goai.go b/protocol/goai/goai.go index b94e7ba4b..bcf130100 100644 --- a/protocol/goai/goai.go +++ b/protocol/goai/goai.go @@ -41,6 +41,7 @@ type ExternalDocs struct { } const ( + HttpMethodAll = `ALL` HttpMethodGet = `GET` HttpMethodPut = `PUT` HttpMethodPost = `POST` diff --git a/protocol/goai/goai_path.go b/protocol/goai/goai_path.go index 871084ab5..e6f9c933e 100644 --- a/protocol/goai/goai_path.go +++ b/protocol/goai/goai_path.go @@ -92,25 +92,27 @@ func (oai *OpenApiV3) addPath(in addPathInput) error { Responses: map[string]ResponseRef{}, } ) + // Path check. if in.Path == "" { in.Path = gmeta.Get(inputObject.Interface(), TagNamePath).String() } if in.Path == "" { - panic(gerror.NewCode( + return gerror.NewCode( gcode.CodeMissingParameter, - `missing necessary path parameter "%s" for struct "%s"`, + `missing necessary path parameter "%s" for input struct "%s"`, TagNamePath, inputStructTypeName, - )) + ) } + // Method check. if in.Method == "" { in.Method = gmeta.Get(inputObject.Interface(), TagNameMethod).String() } if in.Method == "" { - panic(gerror.NewCode( + return gerror.NewCode( gcode.CodeMissingParameter, - `missing necessary method parameter "%s" for struct "%s"`, + `missing necessary method parameter "%s" for input struct "%s"`, TagNamePath, inputStructTypeName, - )) + ) } if err := oai.addSchema(inputObject.Interface(), outputObject.Interface()); err != nil { @@ -189,25 +191,38 @@ func (oai *OpenApiV3) addPath(in addPathInput) error { // Assign to certain operation attribute. switch gstr.ToUpper(in.Method) { case HttpMethodGet: + // GET operations cannot have a requestBody. + operation.RequestBody.Value = nil path.Get = &operation + case HttpMethodPut: path.Put = &operation + case HttpMethodPost: path.Post = &operation + case HttpMethodDelete: + // DELETE operations cannot have a requestBody. + operation.RequestBody.Value = nil path.Delete = &operation + case HttpMethodConnect: - path.Connect = &operation + // Nothing to do for Connect. + case HttpMethodHead: path.Head = &operation + case HttpMethodOptions: path.Options = &operation + case HttpMethodPatch: path.Patch = &operation + case HttpMethodTrace: path.Trace = &operation + default: - panic(gerror.NewCode(gcode.CodeInvalidParameter, `invalid method "%s"`, in.Method)) + return gerror.NewCodef(gcode.CodeInvalidParameter, `invalid method "%s"`, in.Method) } oai.Paths[in.Path] = path return nil