Skip to content

Commit

Permalink
Description option : override + default description contains the list…
Browse files Browse the repository at this point in the history
… of middlewares (#275)

* Refactored the Description/AddDescription to a clearer system

* New more explicit OverrideDescription option

* Tests for the description option with middlewares

* Make tag order deterministic

* Update option.go

Co-authored-by: ccoVeille <[email protected]>

* Makes overrideDescription private in BaseRoute

* Moved the operation ID generation to a method on the BaseRoute struct.

* Generate description is in OpenAPI registration, not in mux registration

---------

Co-authored-by: ccoVeille <[email protected]>
  • Loading branch information
EwenQuim and ccoVeille authored Dec 14, 2024
1 parent 79f6d66 commit a256a7c
Show file tree
Hide file tree
Showing 11 changed files with 167 additions and 53 deletions.
4 changes: 3 additions & 1 deletion examples/full-app-gourmet/views/views.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ func (rs Resource) Routes(s *fuego.Server) {
fuego.Get(adminRoutes, "/ingredients/create", rs.adminIngredientCreationPage)
fuego.All(adminRoutes, "/ingredients/{id}", rs.adminOneIngredient)

fuego.Post(adminRoutes, "/ingredients/new", rs.adminCreateIngredient)
fuego.Post(adminRoutes, "/ingredients/new", rs.adminCreateIngredient,
option.Description("Create a new ingredient"),
)
fuego.Get(adminRoutes, "/users", rs.adminRecipes)
}
11 changes: 11 additions & 0 deletions examples/petstore/controllers/middlewares.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package controller

import "net/http"

func dummyMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// do something before
next.ServeHTTP(w, r)
// do something after
})
}
9 changes: 6 additions & 3 deletions examples/petstore/controllers/pets.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,18 @@ func (rs PetsResources) Routes(s *fuego.Server) {
option.Description("Get all pets"),
)

fuego.Get(petsGroup, "/by-age", rs.getAllPetsByAge, option.Description("Returns an array of pets grouped by age"))
fuego.Get(petsGroup, "/by-age", rs.getAllPetsByAge,
option.Description("Returns an array of pets grouped by age"),
option.Middleware(dummyMiddleware),
)
fuego.Post(petsGroup, "/", rs.postPets,
option.DefaultStatusCode(201),
option.AddResponse(409, "Conflict: Pet with the same name already exists", fuego.Response{Type: PetsError{}}),
)

fuego.Get(petsGroup, "/{id}", rs.getPets,
option.OverrideDescription("Replace description with this sentence."),
option.OperationID("getPet"),
option.Path("id", "Pet ID", param.Example("example", "123")),
)
fuego.Get(petsGroup, "/by-name/{name...}", rs.getPetByName)
Expand All @@ -77,14 +82,12 @@ func (rs PetsResources) Routes(s *fuego.Server) {
if err := json.NewEncoder(w).Encode(pets); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}

}, option.AddResponse(http.StatusOK, "all the pets",
fuego.Response{
Type: []models.Pets{},
ContentTypes: []string{"application/json"},
},
))

}

func (rs PetsResources) getAllPets(c fuego.ContextNoBody) ([]models.Pets, error) {
Expand Down
28 changes: 14 additions & 14 deletions examples/petstore/lib/testdata/doc/openapi.golden.json
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@
"paths": {
"/pets/": {
"get": {
"description": "controller: `github.com/go-fuego/fuego/examples/petstore/controllers.PetsResources.filterPets`\n\n---\n\nFilter pets",
"description": "#### Controller: \n\n`github.com/go-fuego/fuego/examples/petstore/controllers.PetsResources.filterPets`\n\n---\n\nFilter pets",
"operationId": "GET_/pets/",
"parameters": [
{
Expand Down Expand Up @@ -302,7 +302,7 @@
]
},
"post": {
"description": "controller: `github.com/go-fuego/fuego/examples/petstore/controllers.PetsResources.postPets`\n\n---\n\n",
"description": "#### Controller: \n\n`github.com/go-fuego/fuego/examples/petstore/controllers.PetsResources.postPets`\n\n---\n\n",
"operationId": "POST_/pets/",
"parameters": [
{
Expand Down Expand Up @@ -395,7 +395,7 @@
},
"/pets/all": {
"get": {
"description": "controller: `github.com/go-fuego/fuego/examples/petstore/controllers.PetsResources.getAllPets`\n\n---\n\nGet all pets",
"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": [
{
Expand Down Expand Up @@ -525,7 +525,7 @@
},
"/pets/by-age": {
"get": {
"description": "controller: `github.com/go-fuego/fuego/examples/petstore/controllers.PetsResources.getAllPetsByAge`\n\n---\n\nReturns an array of pets grouped by age",
"description": "#### Controller: \n\n`github.com/go-fuego/fuego/examples/petstore/controllers.PetsResources.getAllPetsByAge`\n\n#### Middlewares:\n\n- `github.com/go-fuego/fuego/examples/petstore/controllers.dummyMiddleware`\n\n---\n\nReturns an array of pets grouped by age",
"operationId": "GET_/pets/by-age",
"parameters": [
{
Expand Down Expand Up @@ -604,7 +604,7 @@
},
"/pets/by-name/{name...}": {
"get": {
"description": "controller: `github.com/go-fuego/fuego/examples/petstore/controllers.PetsResources.getPetByName`\n\n---\n\n",
"description": "#### Controller: \n\n`github.com/go-fuego/fuego/examples/petstore/controllers.PetsResources.getPetByName`\n\n---\n\n",
"operationId": "GET_/pets/by-name/:name...",
"parameters": [
{
Expand Down Expand Up @@ -680,7 +680,7 @@
},
"/pets/std/all": {
"get": {
"description": "controller: `github.com/go-fuego/fuego/examples/petstore/controllers.PetsResources.Routes.func1`\n\n---\n\n",
"description": "#### Controller: \n\n`github.com/go-fuego/fuego/examples/petstore/controllers.PetsResources.Routes.func1`\n\n---\n\n",
"operationId": "GET_/pets/std/all",
"parameters": [
{
Expand Down Expand Up @@ -739,7 +739,7 @@
},
"/pets/{id}": {
"delete": {
"description": "controller: `github.com/go-fuego/fuego/examples/petstore/controllers.PetsResources.deletePets`\n\n---\n\n",
"description": "#### Controller: \n\n`github.com/go-fuego/fuego/examples/petstore/controllers.PetsResources.deletePets`\n\n---\n\n",
"operationId": "DELETE_/pets/:id",
"parameters": [
{
Expand Down Expand Up @@ -812,8 +812,8 @@
]
},
"get": {
"description": "controller: `github.com/go-fuego/fuego/examples/petstore/controllers.PetsResources.getPets`\n\n---\n\n",
"operationId": "GET_/pets/:id",
"description": "Replace description with this sentence.",
"operationId": "getPet",
"parameters": [
{
"in": "header",
Expand Down Expand Up @@ -891,7 +891,7 @@
]
},
"put": {
"description": "controller: `github.com/go-fuego/fuego/examples/petstore/controllers.PetsResources.putPets`\n\n---\n\n",
"description": "#### Controller: \n\n`github.com/go-fuego/fuego/examples/petstore/controllers.PetsResources.putPets`\n\n---\n\n",
"operationId": "PUT_/pets/:id",
"parameters": [
{
Expand Down Expand Up @@ -977,7 +977,7 @@
},
"/pets/{id}/json": {
"put": {
"description": "controller: `github.com/go-fuego/fuego/examples/petstore/controllers.PetsResources.putPets`\n\n---\n\n",
"description": "#### Controller: \n\n`github.com/go-fuego/fuego/examples/petstore/controllers.PetsResources.putPets`\n\n---\n\n",
"operationId": "PUT_/pets/:id/json",
"parameters": [
{
Expand Down Expand Up @@ -1070,13 +1070,13 @@
],
"tags": [
{
"name": "pets"
"name": "my-tag"
},
{
"name": "std"
"name": "pets"
},
{
"name": "my-tag"
"name": "std"
}
]
}
63 changes: 42 additions & 21 deletions mux.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,19 @@ type BaseRoute struct {
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.
Expand Down Expand Up @@ -93,41 +106,26 @@ func Register[T, B any](s *Server, route Route[T, B], controller http.Handler, o
o(&route.BaseRoute)
}
route.Handler = controller
route.Path = s.basePath + route.Path

fullPath := s.basePath + route.Path
fullPath := route.Path
if route.Method != "" {
fullPath = route.Method + " " + fullPath
}
slog.Debug("registering controller " + fullPath)

allMiddlewares := append(s.middlewares, route.Middlewares...)
s.Mux.Handle(fullPath, withMiddlewares(route.Handler, allMiddlewares...))
route.Middlewares = append(s.middlewares, route.Middlewares...)
s.Mux.Handle(fullPath, withMiddlewares(route.Handler, route.Middlewares...))

if s.DisableOpenapi || route.Hidden || route.Method == "" {
return &route
}

route.Path = s.basePath + route.Path

err := route.RegisterOpenAPIOperation(s.OpenAPI)
if err != nil {
slog.Warn("error documenting openapi operation", "error", err)
}

if route.FullName == "" {
route.FullName = route.Path
}

if route.Operation.Summary == "" {
route.Operation.Summary = route.NameFromNamespace(camelToHuman)
}

route.Operation.Description = "controller: `" + route.FullName + "`\n\n---\n\n" + route.Operation.Description

if route.Operation.OperationID == "" {
route.Operation.OperationID = route.Method + "_" + strings.ReplaceAll(strings.ReplaceAll(route.Path, "{", ":"), "}", "")
}

return &route
}

Expand Down Expand Up @@ -180,7 +178,7 @@ func registerFuegoController[T, B any, Contexted ctx[B]](s *Server, method, path
Path: path,
Params: make(map[string]OpenAPIParam),
FullName: FuncName(controller),
Operation: openapi3.NewOperation(),
Operation: &openapi3.Operation{},
OpenAPI: s.OpenAPI,
}

Expand All @@ -199,8 +197,10 @@ func registerStdController(s *Server, method, path string, controller func(http.
route := BaseRoute{
Method: method,
Path: path,
Params: make(map[string]OpenAPIParam),
FullName: FuncName(controller),
Operation: openapi3.NewOperation(),
Operation: &openapi3.Operation{},
Handler: http.HandlerFunc(controller),
OpenAPI: s.OpenAPI,
}

Expand Down Expand Up @@ -253,3 +253,24 @@ func camelToHuman(s string) string {
}
return result.String()
}

// DefaultDescription returns a default .md description for a controller
func DefaultDescription[T any](handler string, middlewares []T) string {
description := "#### Controller: \n\n`" +
handler + "`"

if len(middlewares) > 0 {
description += "\n\n#### Middlewares:\n"

for i, fn := range middlewares {
description += "\n- `" + FuncName(fn) + "`"

if i == 4 {
description += "\n- more middleware..."
break
}
}
}

return description + "\n\n---\n\n"
}
6 changes: 3 additions & 3 deletions mux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -402,21 +402,21 @@ func TestRegister(t *testing.T) {
Operation: &openapi3.Operation{
Tags: []string{"my-tag"},
Summary: "my-summary",
Description: "my-description",
Description: "my-description\n",
OperationID: "my-operation-id",
},
},
}, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}),
OptionOperationID("new-operation-id"),
OptionSummary("new-summary"),
OptionDescription("new-description"),
OptionOverrideDescription("new-description"),
OptionTags("new-tag"),
)

require.NotNil(t, route)
require.Equal(t, []string{"my-tag", "new-tag"}, route.Operation.Tags)
require.Equal(t, "new-summary", route.Operation.Summary)
require.Equal(t, "controller: `/test`\n\n---\n\nnew-description", route.Operation.Description)
require.Equal(t, "new-description", route.Operation.Description)
require.Equal(t, "new-operation-id", route.Operation.OperationID)
})
}
Expand Down
21 changes: 20 additions & 1 deletion openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ func declareAllTagsFromOperations(s *Server) {
}
}
}

// Make sure tags are sorted
slices.SortFunc(s.OpenAPI.Description().Tags, func(a, b *openapi3.Tag) int {
return strings.Compare(a.Name, b.Name)
})
}

// OutputOpenAPISpec takes the OpenAPI spec and outputs it to a JSON file and/or serves it on a URL.
Expand Down Expand Up @@ -209,6 +214,20 @@ func RegisterOpenAPIOperation[T, B any](openapi *OpenAPI, route Route[T, B]) (*o
route.Operation = openapi3.NewOperation()
}

if route.FullName == "" {
route.FullName = route.Path
}

route.GenerateDefaultDescription()

if route.Operation.Summary == "" {
route.Operation.Summary = route.NameFromNamespace(camelToHuman)
}

if route.Operation.OperationID == "" {
route.GenerateDefaultOperationID()
}

// Request Body
if route.Operation.RequestBody == nil {
bodyTag := SchemaTagFromType(openapi, *new(B))
Expand Down Expand Up @@ -263,7 +282,7 @@ func RegisterOpenAPIOperation[T, B any](openapi *OpenAPI, route Route[T, B]) (*o
for _, params := range route.Operation.Parameters {
if params.Value.In == "path" {
if !strings.Contains(route.Path, "{"+params.Value.Name) {
return nil, fmt.Errorf("path parameter '%s' is not declared in the path", params.Value.Name)
panic(fmt.Errorf("path parameter '%s' is not declared in the path", params.Value.Name))
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion openapi_operations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func TestTags(t *testing.T) {
)

require.Equal(t, []string{"my-tag"}, route.Operation.Tags)
require.Equal(t, "controller: `github.com/go-fuego/fuego.testController`\n\n---\n\nmy description", route.Operation.Description)
require.Equal(t, "#### Controller: \n\n`github.com/go-fuego/fuego.testController`\n\n---\n\nmy description", route.Operation.Description)
require.Equal(t, "my summary", route.Operation.Summary)
require.Equal(t, true, route.Operation.Deprecated)
}
Expand Down
16 changes: 13 additions & 3 deletions option.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,10 +258,10 @@ func OptionSummary(summary string) func(*BaseRoute) {
}
}

// Description sets the description to the route.
// Description adds a description to the route.
// By default, the description is set by Fuego with some info,
// like the controller function name and the package name.
// If you want to add a description, please use [AddDescription] instead.
// If you want to override Fuego's description, please use [OptionOverrideDescription] instead.
func OptionDescription(description string) func(*BaseRoute) {
return func(r *BaseRoute) {
r.Operation.Description = description
Expand All @@ -273,7 +273,17 @@ func OptionDescription(description string) func(*BaseRoute) {
// like the controller function name and the package name.
func OptionAddDescription(description string) func(*BaseRoute) {
return func(r *BaseRoute) {
r.Operation.Description += "\n\n" + description
r.Operation.Description += description
}
}

// OptionOverrideDescription overrides the default description set by Fuego.
// By default, the description is set by Fuego with some info,
// like the controller function name and the package name.
func OptionOverrideDescription(description string) func(*BaseRoute) {
return func(r *BaseRoute) {
r.overrideDescription = true
r.Operation.Description = description
}
}

Expand Down
Loading

0 comments on commit a256a7c

Please sign in to comment.