diff --git a/examples/petstore/controllers/pets.go b/examples/petstore/controllers/pets.go index 6a8a63cd..8ca7245e 100644 --- a/examples/petstore/controllers/pets.go +++ b/examples/petstore/controllers/pets.go @@ -46,6 +46,7 @@ func (rs PetsResources) Routes(s *fuego.Server) { fuego.Get(petsGroup, "/by-age", rs.getAllPetsByAge, option.Description("Returns an array of pets grouped by age")) fuego.Post(petsGroup, "/", rs.postPets, + option.DefaultStatusCode(201), option.AddError(409, "Conflict: Pet with the same name already exists", PetsError{}), ) diff --git a/examples/petstore/controllers/pets_test.go b/examples/petstore/controllers/pets_test.go index 919b9921..d70478d3 100644 --- a/examples/petstore/controllers/pets_test.go +++ b/examples/petstore/controllers/pets_test.go @@ -51,7 +51,10 @@ func TestPostPets(t *testing.T) { s.Mux.ServeHTTP(w, r) - require.Equal(t, http.StatusOK, w.Code) + require.Equal(t, http.StatusCreated, w.Code) + petId := w.Body.String() + t.Log(petId) + require.NotEmpty(t, petId) }) } @@ -63,7 +66,7 @@ func TestGetPets(t *testing.T) { r := httptest.NewRequest("POST", "/pets/", strings.NewReader(`{"name": "kitkat"}`)) s.Mux.ServeHTTP(w, r) t.Log(w.Body.String()) - require.Equal(t, http.StatusOK, w.Code) + require.Equal(t, http.StatusCreated, w.Code) w = httptest.NewRecorder() r = httptest.NewRequest("GET", "/pets/pet-1", nil) @@ -83,7 +86,7 @@ func TestGetAllPestByAge(t *testing.T) { r := httptest.NewRequest("POST", "/pets/", strings.NewReader(`{"name": "kitkat"}`)) s.Mux.ServeHTTP(w, r) t.Log(w.Body.String()) - require.Equal(t, http.StatusOK, w.Code) + require.Equal(t, http.StatusCreated, w.Code) w = httptest.NewRecorder() r = httptest.NewRequest("GET", "/pets/by-age", nil) @@ -103,7 +106,7 @@ func TestGetPetsByName(t *testing.T) { r := httptest.NewRequest("POST", "/pets/", strings.NewReader(`{"name": "kitkat"}`)) s.Mux.ServeHTTP(w, r) t.Log(w.Body.String()) - require.Equal(t, http.StatusOK, w.Code) + require.Equal(t, http.StatusCreated, w.Code) w = httptest.NewRecorder() r = httptest.NewRequest("GET", "/pets/by-name/kitkat", nil) @@ -123,7 +126,7 @@ func TestPutPets(t *testing.T) { r := httptest.NewRequest("POST", "/pets/", strings.NewReader(`{"name": "kitkat"}`)) s.Mux.ServeHTTP(w, r) t.Log(w.Body.String()) - require.Equal(t, http.StatusOK, w.Code) + require.Equal(t, http.StatusCreated, w.Code) w = httptest.NewRecorder() r = httptest.NewRequest("PUT", "/pets/pet-1", strings.NewReader(`{"name": "snickers"}`)) @@ -143,7 +146,7 @@ func TestDeletePets(t *testing.T) { r := httptest.NewRequest("POST", "/pets/", strings.NewReader(`{"name": "kitkat"}`)) s.Mux.ServeHTTP(w, r) t.Log(w.Body.String()) - require.Equal(t, http.StatusOK, w.Code) + require.Equal(t, http.StatusCreated, w.Code) w = httptest.NewRecorder() r = httptest.NewRequest("DELETE", "/pets/pet-1", nil) diff --git a/examples/petstore/lib/testdata/doc/openapi.golden.json b/examples/petstore/lib/testdata/doc/openapi.golden.json index 56c957a1..853aa99f 100644 --- a/examples/petstore/lib/testdata/doc/openapi.golden.json +++ b/examples/petstore/lib/testdata/doc/openapi.golden.json @@ -306,7 +306,7 @@ "required": true }, "responses": { - "200": { + "201": { "content": { "application/json": { "schema": { @@ -319,7 +319,7 @@ } } }, - "description": "OK" + "description": "Created" }, "400": { "content": { diff --git a/mux.go b/mux.go index b07c8e21..1a7aa23a 100644 --- a/mux.go +++ b/mux.go @@ -59,6 +59,7 @@ type BaseRoute struct { 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 mainRouter *Server // ref to the main router, used to register the route in the OpenAPI spec } diff --git a/openapi.go b/openapi.go index 072a719a..dc40087f 100644 --- a/openapi.go +++ b/openapi.go @@ -188,19 +188,23 @@ func RegisterOpenAPIOperation[T, B any](s *Server, route Route[T, B]) (*openapi3 addResponseIfNotSet(s, route.Operation, openAPIGlobalResponse.Code, openAPIGlobalResponse.Description, openAPIGlobalResponse.ErrorType) } - // Automatically add non-declared 200 Response - response200 := route.Operation.Responses.Value("200") - if response200 == nil { - response := openapi3.NewResponse().WithDescription("OK") - route.Operation.AddResponse(200, response) - response200 = route.Operation.Responses.Value("200") + // Automatically add non-declared 200 (or other) Response + if route.DefaultStatusCode == 0 { + route.DefaultStatusCode = 200 + } + defaultStatusCode := strconv.Itoa(route.DefaultStatusCode) + responseDefault := route.Operation.Responses.Value(defaultStatusCode) + if responseDefault == nil { + response := openapi3.NewResponse().WithDescription(http.StatusText(route.DefaultStatusCode)) + route.Operation.AddResponse(route.DefaultStatusCode, response) + responseDefault = route.Operation.Responses.Value(defaultStatusCode) } - // Automatically add non-declared Content for 200 Response - if response200.Value.Content == nil { + // Automatically add non-declared Content for 200 (or other) Response + if responseDefault.Value.Content == nil { responseSchema := SchemaTagFromType(s, *new(T)) content := openapi3.NewContentWithSchemaRef(&responseSchema.SchemaRef, []string{"application/json", "application/xml"}) - response200.Value.WithContent(content) + responseDefault.Value.WithContent(content) } // Automatically add non-declared Path parameters diff --git a/option.go b/option.go index 7f2a72df..1ec14a1e 100644 --- a/option.go +++ b/option.go @@ -287,6 +287,13 @@ func OptionHide() func(*BaseRoute) { } } +// Hide hides the route from the OpenAPI spec. +func OptionDefaultStatusCode(defaultStatusCode int) func(*BaseRoute) { + return func(r *BaseRoute) { + r.DefaultStatusCode = defaultStatusCode + } +} + // OptionSecurity configures security requirements to the route. // // Single Scheme (AND Logic): diff --git a/option/option.go b/option/option.go index fcaebaff..151dffda 100644 --- a/option/option.go +++ b/option/option.go @@ -143,3 +143,5 @@ var RequestContentType = fuego.OptionRequestContentType // Hide hides the route from the OpenAPI spec. var Hide = fuego.OptionHide + +var DefaultStatusCode = fuego.OptionDefaultStatusCode diff --git a/option_test.go b/option_test.go index 600d628e..197abdd0 100644 --- a/option_test.go +++ b/option_test.go @@ -695,3 +695,44 @@ func TestOptionAddDescription(t *testing.T) { require.Equal(t, "controller: `github.com/go-fuego/fuego_test.helloWorld`\n\n---\n\ntest description\n\nanother description", route.Operation.Description) }) } + +func TestDefaultStatusCode(t *testing.T) { + t.Run("Declare a default status code for the route", func(t *testing.T) { + s := fuego.NewServer() + + route := fuego.Post(s, "/test", helloWorld, + fuego.OptionDefaultStatusCode(201), + ) + + r := httptest.NewRequest(http.MethodPost, "/test", nil) + w := httptest.NewRecorder() + + s.Mux.ServeHTTP(w, r) + + require.Equal(t, 201, w.Code) + require.Equal(t, "hello world", w.Body.String()) + require.Equal(t, 201, route.DefaultStatusCode) + require.NotNil(t, route.Operation.Responses.Value("201").Value) + }) + + t.Run("Declare a default status code for the route but bypass it in the controller", func(t *testing.T) { + s := fuego.NewServer() + + route := fuego.Post(s, "/test", func(c fuego.ContextNoBody) (string, error) { + c.SetStatus(200) + return "hello world", nil + }, + fuego.OptionDefaultStatusCode(201), + ) + + r := httptest.NewRequest(http.MethodPost, "/test", nil) + w := httptest.NewRecorder() + + s.Mux.ServeHTTP(w, r) + + require.Equal(t, 200, w.Code) + require.Equal(t, "hello world", w.Body.String()) + require.Equal(t, 201, route.DefaultStatusCode, "default status code should not be changed") + require.NotNil(t, route.Operation.Responses.Value("201").Value, "default status is still in the spec even if code is not used") + }) +} diff --git a/serve.go b/serve.go index 91fdba90..b410b284 100644 --- a/serve.go +++ b/serve.go @@ -139,6 +139,10 @@ func HTTPHandler[ReturnType, Body any, Contextable ctx[Body]](s *Server, control return } + if route.DefaultStatusCode != 0 { + w.WriteHeader(route.DefaultStatusCode) + } + // TRANSFORM OUT timeTransformOut := time.Now() ans, err = transformOut(r.Context(), ans)