From c3bcab3e042ec0f2723dc8ba1c0117ac64a7b14f Mon Sep 17 00:00:00 2001 From: EwenQuim Date: Mon, 16 Dec 2024 17:31:32 +0100 Subject: [PATCH] Refactor, mutualize and document Route object --- .../lib/testdata/doc/openapi.golden.json | 78 +++++++++---------- mux.go | 62 +-------------- route.go | 67 ++++++++++++++++ server_test.go | 20 ++--- 4 files changed, 120 insertions(+), 107 deletions(-) create mode 100644 route.go diff --git a/examples/petstore/lib/testdata/doc/openapi.golden.json b/examples/petstore/lib/testdata/doc/openapi.golden.json index 167c6f2e..44431594 100644 --- a/examples/petstore/lib/testdata/doc/openapi.golden.json +++ b/examples/petstore/lib/testdata/doc/openapi.golden.json @@ -155,13 +155,6 @@ "description": "#### Controller: \n\n`github.com/go-fuego/fuego/examples/petstore/controllers.PetsResources.filterPets`\n\n---\n\nFilter pets", "operationId": "GET_/pets/", "parameters": [ - { - "in": "header", - "name": "Accept", - "schema": { - "type": "string" - } - }, { "description": "header description", "in": "header", @@ -221,6 +214,13 @@ "default": 3, "type": "integer" } + }, + { + "in": "header", + "name": "Accept", + "schema": { + "type": "string" + } } ], "responses": { @@ -306,16 +306,16 @@ "operationId": "POST_/pets/", "parameters": [ { + "description": "header description", "in": "header", - "name": "Accept", + "name": "X-Header", "schema": { "type": "string" } }, { - "description": "header description", "in": "header", - "name": "X-Header", + "name": "Accept", "schema": { "type": "string" } @@ -398,13 +398,6 @@ "description": "#### Controller: \n\n`github.com/go-fuego/fuego/examples/petstore/controllers.PetsResources.getAllPets`\n\n---\n\nGet all pets", "operationId": "GET_/pets/all", "parameters": [ - { - "in": "header", - "name": "Accept", - "schema": { - "type": "string" - } - }, { "description": "header description", "in": "header", @@ -441,6 +434,13 @@ "default": 1, "type": "integer" } + }, + { + "in": "header", + "name": "Accept", + "schema": { + "type": "string" + } } ], "responses": { @@ -529,16 +529,16 @@ "operationId": "GET_/pets/by-age", "parameters": [ { + "description": "header description", "in": "header", - "name": "Accept", + "name": "X-Header", "schema": { "type": "string" } }, { - "description": "header description", "in": "header", - "name": "X-Header", + "name": "Accept", "schema": { "type": "string" } @@ -608,16 +608,16 @@ "operationId": "GET_/pets/by-name/:name...", "parameters": [ { + "description": "header description", "in": "header", - "name": "Accept", + "name": "X-Header", "schema": { "type": "string" } }, { - "description": "header description", "in": "header", - "name": "X-Header", + "name": "Accept", "schema": { "type": "string" } @@ -743,16 +743,16 @@ "operationId": "DELETE_/pets/:id", "parameters": [ { + "description": "header description", "in": "header", - "name": "Accept", + "name": "X-Header", "schema": { "type": "string" } }, { - "description": "header description", "in": "header", - "name": "X-Header", + "name": "Accept", "schema": { "type": "string" } @@ -815,13 +815,6 @@ "description": "Replace description with this sentence.", "operationId": "getPet", "parameters": [ - { - "in": "header", - "name": "Accept", - "schema": { - "type": "string" - } - }, { "description": "header description", "in": "header", @@ -843,6 +836,13 @@ "schema": { "type": "string" } + }, + { + "in": "header", + "name": "Accept", + "schema": { + "type": "string" + } } ], "responses": { @@ -895,16 +895,16 @@ "operationId": "PUT_/pets/:id", "parameters": [ { + "description": "header description", "in": "header", - "name": "Accept", + "name": "X-Header", "schema": { "type": "string" } }, { - "description": "header description", "in": "header", - "name": "X-Header", + "name": "Accept", "schema": { "type": "string" } @@ -981,16 +981,16 @@ "operationId": "PUT_/pets/:id/json", "parameters": [ { + "description": "header description", "in": "header", - "name": "Accept", + "name": "X-Header", "schema": { "type": "string" } }, { - "description": "header description", "in": "header", - "name": "X-Header", + "name": "Accept", "schema": { "type": "string" } diff --git a/mux.go b/mux.go index 7b348708..18b4f642 100644 --- a/mux.go +++ b/mux.go @@ -44,37 +44,6 @@ func Group(s *Server, path string, routeOptions ...func(*BaseRoute)) *Server { return newServer } -type Route[ResponseBody any, RequestBody any] struct { - BaseRoute -} - -type BaseRoute struct { - Operation *openapi3.Operation // GENERATED OpenAPI operation, do not set manually in Register function. You can change it after the route is registered. - Method string // HTTP method (GET, POST, PUT, PATCH, DELETE) - Path string // URL path. Will be prefixed by the base path of the server and the group path if any - Handler http.Handler // handler executed for this route - FullName string // namespace and name of the function to execute - Params map[string]OpenAPIParam - Middlewares []func(http.Handler) http.Handler - AcceptedContentTypes []string // Content types accepted for the request body. If nil, all content types (*/*) are accepted. - Hidden bool // If true, the route will not be documented in the OpenAPI spec - DefaultStatusCode int // Default status code for the response - OpenAPI *OpenAPI // Ref to the whole OpenAPI spec - - overrideDescription bool // Override the default description -} - -func (r *BaseRoute) GenerateDefaultDescription() { - if r.overrideDescription { - return - } - r.Operation.Description = DefaultDescription(r.FullName, r.Middlewares) + r.Operation.Description -} - -func (r *BaseRoute) GenerateDefaultOperationID() { - r.Operation.OperationID = r.Method + "_" + strings.ReplaceAll(strings.ReplaceAll(r.Path, "{", ":"), "}", "") -} - // Capture all methods (GET, POST, PUT, PATCH, DELETE) and register a controller. func All[ReturnType, Body any, Contexted ctx[Body]](s *Server, path string, controller func(Contexted) (ReturnType, error), options ...func(*BaseRoute)) *Route[ReturnType, Body] { return registerFuegoController(s, "", path, controller, options...) @@ -173,42 +142,19 @@ func PatchStd(s *Server, path string, controller func(http.ResponseWriter, *http } func registerFuegoController[T, B any, Contexted ctx[B]](s *Server, method, path string, controller func(Contexted) (T, error), options ...func(*BaseRoute)) *Route[T, B] { - route := BaseRoute{ - Method: method, - Path: path, - Params: make(map[string]OpenAPIParam), - FullName: FuncName(controller), - Operation: &openapi3.Operation{}, - OpenAPI: s.OpenAPI, - } + route := NewRoute[T, B](method, path, controller, s.OpenAPI, append(s.routeOptions, options...)...) acceptHeaderParameter := openapi3.NewHeaderParameter("Accept") acceptHeaderParameter.Schema = openapi3.NewStringSchema().NewRef() route.Operation.AddParameter(acceptHeaderParameter) - for _, o := range append(s.routeOptions, options...) { - o(&route) - } - - return Register(s, Route[T, B]{BaseRoute: route}, HTTPHandler(s, controller, &route)) + return Register(s, route, HTTPHandler(s, controller, &route.BaseRoute)) } func registerStdController(s *Server, method, path string, controller func(http.ResponseWriter, *http.Request), options ...func(*BaseRoute)) *Route[any, any] { - route := BaseRoute{ - Method: method, - Path: path, - Params: make(map[string]OpenAPIParam), - FullName: FuncName(controller), - Operation: &openapi3.Operation{}, - Handler: http.HandlerFunc(controller), - OpenAPI: s.OpenAPI, - } - - for _, o := range append(s.routeOptions, options...) { - o(&route) - } + route := NewRoute[any, any](method, path, controller, s.OpenAPI, append(s.routeOptions, options...)...) - return Register(s, Route[any, any]{BaseRoute: route}, http.HandlerFunc(controller)) + return Register(s, route, http.HandlerFunc(controller)) } func withMiddlewares(controller http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler { diff --git a/route.go b/route.go new file mode 100644 index 00000000..3759ca81 --- /dev/null +++ b/route.go @@ -0,0 +1,67 @@ +package fuego + +import ( + "net/http" + "strings" + + "github.com/getkin/kin-openapi/openapi3" +) + +func NewRoute[T, B any](method, path string, handler any, openapi *OpenAPI, options ...func(*BaseRoute)) Route[T, B] { + return Route[T, B]{ + BaseRoute: NewBaseRoute(method, path, handler, openapi, options...), + } +} + +// Route is the main struct for a route in Fuego. +// It contains the OpenAPI operation and other metadata. +// It is a wrapper around BaseRoute, with the addition of the response and request body types. +type Route[ResponseBody any, RequestBody any] struct { + BaseRoute +} + +func NewBaseRoute(method, path string, handler any, openapi *OpenAPI, options ...func(*BaseRoute)) BaseRoute { + baseRoute := BaseRoute{ + Method: method, + Path: path, + Params: make(map[string]OpenAPIParam), + FullName: FuncName(handler), + Operation: openapi3.NewOperation(), + OpenAPI: openapi, + } + + for _, o := range options { + o(&baseRoute) + } + + return baseRoute +} + +// BaseRoute is the base struct for all routes in Fuego. +// It contains the OpenAPI operation and other metadata. +type BaseRoute struct { + Operation *openapi3.Operation // GENERATED OpenAPI operation, do not set manually in Register function. You can change it after the route is registered. + Method string // HTTP method (GET, POST, PUT, PATCH, DELETE) + Path string // URL path. Will be prefixed by the base path of the server and the group path if any + Handler http.Handler // handler executed for this route + FullName string // namespace and name of the function to execute + Params map[string]OpenAPIParam + Middlewares []func(http.Handler) http.Handler + AcceptedContentTypes []string // Content types accepted for the request body. If nil, all content types (*/*) are accepted. + Hidden bool // If true, the route will not be documented in the OpenAPI spec + DefaultStatusCode int // Default status code for the response + OpenAPI *OpenAPI // Ref to the whole OpenAPI spec + + overrideDescription bool // Override the default description +} + +func (r *BaseRoute) GenerateDefaultDescription() { + if r.overrideDescription { + return + } + r.Operation.Description = DefaultDescription(r.FullName, r.Middlewares) + r.Operation.Description +} + +func (r *BaseRoute) GenerateDefaultOperationID() { + r.Operation.OperationID = r.Method + "_" + strings.ReplaceAll(strings.ReplaceAll(r.Path, "{", ":"), "}", "") +} diff --git a/server_test.go b/server_test.go index 671cfc5e..bcaa08e3 100644 --- a/server_test.go +++ b/server_test.go @@ -378,10 +378,10 @@ func TestGroupParams(t *testing.T) { t.Log(document.Paths.Find("/").Get.Parameters[0].Value.Name) require.Len(t, document.Paths.Find("/").Get.Parameters, 1) require.Equal(t, document.Paths.Find("/").Get.Parameters[0].Value.Name, "Accept") - require.Equal(t, document.Paths.Find("/api/test").Get.Parameters[0].Value.Name, "Accept") - require.Equal(t, document.Paths.Find("/api/test").Get.Parameters[1].Value.Name, "X-Test-Header") - require.Equal(t, document.Paths.Find("/api/test2").Get.Parameters[0].Value.Name, "Accept") - require.Equal(t, document.Paths.Find("/api/test2").Get.Parameters[1].Value.Name, "X-Test-Header") + require.Equal(t, document.Paths.Find("/api/test").Get.Parameters[1].Value.Name, "Accept") + require.Equal(t, document.Paths.Find("/api/test").Get.Parameters[0].Value.Name, "X-Test-Header") + require.Equal(t, document.Paths.Find("/api/test2").Get.Parameters[1].Value.Name, "Accept") + require.Equal(t, document.Paths.Find("/api/test2").Get.Parameters[0].Value.Name, "X-Test-Header") } func TestGroupHeaderParams(t *testing.T) { @@ -395,8 +395,8 @@ func TestGroupHeaderParams(t *testing.T) { require.Equal(t, "test-value", route.Operation.Parameters.GetByInAndName("header", "X-Test-Header").Description) document := s.OutputOpenAPISpec() - require.Equal(t, document.Paths.Find("/api/test").Get.Parameters[0].Value.Name, "Accept") - require.Equal(t, document.Paths.Find("/api/test").Get.Parameters[1].Value.Name, "X-Test-Header") + require.Equal(t, document.Paths.Find("/api/test").Get.Parameters[1].Value.Name, "Accept") + require.Equal(t, document.Paths.Find("/api/test").Get.Parameters[0].Value.Name, "X-Test-Header") } func TestGroupCookieParams(t *testing.T) { @@ -410,8 +410,8 @@ func TestGroupCookieParams(t *testing.T) { require.Equal(t, "test-value", route.Operation.Parameters.GetByInAndName("cookie", "X-Test-Cookie").Description) document := s.OutputOpenAPISpec() - require.Equal(t, document.Paths.Find("/api/test").Get.Parameters[0].Value.Name, "Accept") - require.Equal(t, document.Paths.Find("/api/test").Get.Parameters[1].Value.Name, "X-Test-Cookie") + require.Equal(t, document.Paths.Find("/api/test").Get.Parameters[1].Value.Name, "Accept") + require.Equal(t, document.Paths.Find("/api/test").Get.Parameters[0].Value.Name, "X-Test-Cookie") } func TestGroupQueryParam(t *testing.T) { @@ -425,8 +425,8 @@ func TestGroupQueryParam(t *testing.T) { require.Equal(t, "test-value", route.Operation.Parameters.GetByInAndName("query", "X-Test-Query").Description) document := s.OutputOpenAPISpec() - require.Equal(t, document.Paths.Find("/api/test").Get.Parameters[0].Value.Name, "Accept") - require.Equal(t, document.Paths.Find("/api/test").Get.Parameters[1].Value.Name, "X-Test-Query") + require.Equal(t, document.Paths.Find("/api/test").Get.Parameters[1].Value.Name, "Accept") + require.Equal(t, document.Paths.Find("/api/test").Get.Parameters[0].Value.Name, "X-Test-Query") } func TestGroupParamsInChildGroup(t *testing.T) {