diff --git a/internal/test/bodies/content_types/text_and_json/text_and_json.gen.go b/internal/test/bodies/content_types/text_and_json/text_and_json.gen.go index 76143f5ab..2417468d4 100644 --- a/internal/test/bodies/content_types/text_and_json/text_and_json.gen.go +++ b/internal/test/bodies/content_types/text_and_json/text_and_json.gen.go @@ -452,7 +452,7 @@ func (response GetTest401TextResponse) VisitGetTestResponse(w http.ResponseWrite w.Header().Set("Content-Type", "text/plain") w.WriteHeader(401) - _, err := w.Write([]byte(response)) + _, err := w.Write([]byte(fmt.Sprint(response))) return err } diff --git a/internal/test/bodies/content_types/text_non_string/config.yaml b/internal/test/bodies/content_types/text_non_string/config.yaml new file mode 100644 index 000000000..a38474dcf --- /dev/null +++ b/internal/test/bodies/content_types/text_non_string/config.yaml @@ -0,0 +1,10 @@ +# yaml-language-server: $schema=../../../../../configuration-schema.json +# From issue-1897 +package: textnonstring +output: text_non_string.gen.go +generate: + std-http-server: true + strict-server: true + models: true +output-options: + skip-prune: true diff --git a/internal/test/bodies/content_types/text_non_string/doc.go b/internal/test/bodies/content_types/text_non_string/doc.go new file mode 100644 index 000000000..75c705b73 --- /dev/null +++ b/internal/test/bodies/content_types/text_non_string/doc.go @@ -0,0 +1,12 @@ +// Package textnonstring verifies strict-server codegen for text/plain +// responses whose schema is a non-string primitive (integer, boolean). +// The generated Visit* method must compile and write the value's decimal / +// literal text form. Before the fix, the generated code did +// []byte(response) on a non-string-underlying type, which either failed to +// compile (integer, boolean) or, on the fiber/iris path, string(response) +// silently produced a single rune instead of the number. +// +// From issue-1897. +package textnonstring + +//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml spec.yaml diff --git a/internal/test/bodies/content_types/text_non_string/fiber/config.yaml b/internal/test/bodies/content_types/text_non_string/fiber/config.yaml new file mode 100644 index 000000000..a820d5872 --- /dev/null +++ b/internal/test/bodies/content_types/text_non_string/fiber/config.yaml @@ -0,0 +1,10 @@ +# yaml-language-server: $schema=../../../../../../configuration-schema.json +# From issue-1897 — fiber (ctx.WriteString) backend coverage +package: textnonstringfiber +output: fiber.gen.go +generate: + fiber-server: true + strict-server: true + models: true +output-options: + skip-prune: true diff --git a/internal/test/bodies/content_types/text_non_string/fiber/doc.go b/internal/test/bodies/content_types/text_non_string/fiber/doc.go new file mode 100644 index 000000000..1350b45e4 --- /dev/null +++ b/internal/test/bodies/content_types/text_non_string/fiber/doc.go @@ -0,0 +1,3 @@ +package textnonstringfiber + +//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml ../spec.yaml diff --git a/internal/test/bodies/content_types/text_non_string/fiber/fiber.gen.go b/internal/test/bodies/content_types/text_non_string/fiber/fiber.gen.go new file mode 100644 index 000000000..268b6894c --- /dev/null +++ b/internal/test/bodies/content_types/text_non_string/fiber/fiber.gen.go @@ -0,0 +1,159 @@ +// Package textnonstringfiber provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.0.0-00010101000000-000000000000 DO NOT EDIT. +package textnonstringfiber + +import ( + "context" + "fmt" + + "github.com/gofiber/fiber/v2" +) + +// ServerInterface represents all server handlers. +type ServerInterface interface { + + // (GET /ping) + GetPing(c *fiber.Ctx) error +} + +// ServerInterfaceWrapper converts contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface + HandlerMiddlewares []HandlerMiddlewareFunc +} + +type MiddlewareFunc fiber.Handler +type HandlerMiddlewareFunc func(c *fiber.Ctx, next fiber.Handler) error + +// GetPing operation middleware +func (siw *ServerInterfaceWrapper) GetPing(c *fiber.Ctx) error { + + handler := func(c *fiber.Ctx) error { + return siw.Handler.GetPing(c) + } + + for i := len(siw.HandlerMiddlewares) - 1; i >= 0; i-- { + m := siw.HandlerMiddlewares[i] + next := handler + handler = func(c *fiber.Ctx) error { + return m(c, next) + } + } + + return handler(c) +} + +// FiberServerOptions provides options for the Fiber server. +type FiberServerOptions struct { + BaseURL string + Middlewares []MiddlewareFunc + HandlerMiddlewares []HandlerMiddlewareFunc +} + +// RegisterHandlers creates http.Handler with routing matching OpenAPI spec. +func RegisterHandlers(router fiber.Router, si ServerInterface) { + RegisterHandlersWithOptions(router, si, FiberServerOptions{}) +} + +// RegisterHandlersWithOptions creates http.Handler with additional options +func RegisterHandlersWithOptions(router fiber.Router, si ServerInterface, options FiberServerOptions) { + wrapper := ServerInterfaceWrapper{ + Handler: si, + HandlerMiddlewares: options.HandlerMiddlewares, + } + + for _, m := range options.Middlewares { + router.Use(fiber.Handler(m)) + } + + router.Get(options.BaseURL+"/ping", wrapper.GetPing) + +} + +type StatusBoolTextResponse bool + +type StatusIntTextResponse int + +type StatusStrTextResponse string + +type GetPingRequestObject struct { +} + +type GetPingResponseObject interface { + VisitGetPingResponse(ctx *fiber.Ctx) error +} + +type GetPing200TextResponse StatusStrTextResponse + +func (response GetPing200TextResponse) VisitGetPingResponse(ctx *fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "text/plain") + ctx.Status(200) + + _, err := ctx.WriteString(fmt.Sprint(response)) + return err +} + +type GetPing201TextResponse StatusIntTextResponse + +func (response GetPing201TextResponse) VisitGetPingResponse(ctx *fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "text/plain") + ctx.Status(201) + + _, err := ctx.WriteString(fmt.Sprint(response)) + return err +} + +type GetPing202TextResponse StatusBoolTextResponse + +func (response GetPing202TextResponse) VisitGetPingResponse(ctx *fiber.Ctx) error { + ctx.Response().Header.Set("Content-Type", "text/plain") + ctx.Status(202) + + _, err := ctx.WriteString(fmt.Sprint(response)) + return err +} + +// StrictServerInterface represents all server handlers. +type StrictServerInterface interface { + + // (GET /ping) + GetPing(ctx context.Context, request GetPingRequestObject) (GetPingResponseObject, error) +} + +type StrictHandlerFunc func(ctx *fiber.Ctx, args any) (any, error) +type StrictMiddlewareFunc func(f StrictHandlerFunc, operationID string) StrictHandlerFunc + +func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface { + return &strictHandler{ssi: ssi, middlewares: middlewares} +} + +type strictHandler struct { + ssi StrictServerInterface + middlewares []StrictMiddlewareFunc +} + +// GetPing operation middleware +func (sh *strictHandler) GetPing(ctx *fiber.Ctx) error { + var request GetPingRequestObject + + handler := func(ctx *fiber.Ctx, request interface{}) (interface{}, error) { + return sh.ssi.GetPing(ctx.UserContext(), request.(GetPingRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetPing") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(GetPingResponseObject); ok { + if err := validResponse.VisitGetPingResponse(ctx); err != nil { + return err + } + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} diff --git a/internal/test/bodies/content_types/text_non_string/fiber/fiber_test.go b/internal/test/bodies/content_types/text_non_string/fiber/fiber_test.go new file mode 100644 index 000000000..f5d69ae29 --- /dev/null +++ b/internal/test/bodies/content_types/text_non_string/fiber/fiber_test.go @@ -0,0 +1,33 @@ +package textnonstringfiber + +import ( + "context" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/adaptor" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/oapi-codegen/testutil" +) + +type server struct{} + +func (server) GetPing(_ context.Context, _ GetPingRequestObject) (GetPingResponseObject, error) { + return GetPing201TextResponse(201), nil +} + +// TestFiberIntegerTextResponse is the fiber-path (ctx.WriteString) regression +// test for issue-1897: an integer text/plain response must serialize as the +// decimal "201", not the single Unicode code-point that string(response) +// previously produced. +func TestFiberIntegerTextResponse(t *testing.T) { + app := fiber.New() + RegisterHandlers(app, NewStrictHandler(server{}, nil)) + + rr := testutil.NewRequest().Get("/ping").GoWithHTTPHandler(t, adaptor.FiberApp(app)).Recorder + require.Equal(t, 201, rr.Code) + assert.Equal(t, "text/plain", rr.Header().Get("Content-Type")) + assert.Equal(t, "201", rr.Body.String()) +} diff --git a/internal/test/bodies/content_types/text_non_string/iris/config.yaml b/internal/test/bodies/content_types/text_non_string/iris/config.yaml new file mode 100644 index 000000000..a62a09141 --- /dev/null +++ b/internal/test/bodies/content_types/text_non_string/iris/config.yaml @@ -0,0 +1,10 @@ +# yaml-language-server: $schema=../../../../../../configuration-schema.json +# From issue-1897 — iris (ctx.WriteString) backend coverage +package: textnonstringiris +output: iris.gen.go +generate: + iris-server: true + strict-server: true + models: true +output-options: + skip-prune: true diff --git a/internal/test/bodies/content_types/text_non_string/iris/doc.go b/internal/test/bodies/content_types/text_non_string/iris/doc.go new file mode 100644 index 000000000..cf6fa06d2 --- /dev/null +++ b/internal/test/bodies/content_types/text_non_string/iris/doc.go @@ -0,0 +1,3 @@ +package textnonstringiris + +//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml ../spec.yaml diff --git a/internal/test/bodies/content_types/text_non_string/iris/iris.gen.go b/internal/test/bodies/content_types/text_non_string/iris/iris.gen.go new file mode 100644 index 000000000..5d89a6676 --- /dev/null +++ b/internal/test/bodies/content_types/text_non_string/iris/iris.gen.go @@ -0,0 +1,148 @@ +// Package textnonstringiris provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.0.0-00010101000000-000000000000 DO NOT EDIT. +package textnonstringiris + +import ( + "context" + "fmt" + "net/http" + + "github.com/kataras/iris/v12" +) + +// ServerInterface represents all server handlers. +type ServerInterface interface { + + // (GET /ping) + GetPing(ctx iris.Context) +} + +// ServerInterfaceWrapper converts echo contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface +} + +type MiddlewareFunc iris.Handler + +// GetPing converts iris context to params. +func (w *ServerInterfaceWrapper) GetPing(ctx iris.Context) { + + // Invoke the callback with all the unmarshaled arguments + w.Handler.GetPing(ctx) +} + +// IrisServerOption is the option for iris server +type IrisServerOptions struct { + BaseURL string + Middlewares []MiddlewareFunc +} + +// RegisterHandlers creates http.Handler with routing matching OpenAPI spec. +func RegisterHandlers(router *iris.Application, si ServerInterface) { + RegisterHandlersWithOptions(router, si, IrisServerOptions{}) +} + +// RegisterHandlersWithOptions creates http.Handler with additional options +func RegisterHandlersWithOptions(router *iris.Application, si ServerInterface, options IrisServerOptions) { + for _, m := range options.Middlewares { + router.Use(m) + } + + wrapper := ServerInterfaceWrapper{ + Handler: si, + } + + router.Get(options.BaseURL+"/ping", wrapper.GetPing) + + router.Build() +} + +type StatusBoolTextResponse bool + +type StatusIntTextResponse int + +type StatusStrTextResponse string + +type GetPingRequestObject struct { +} + +type GetPingResponseObject interface { + VisitGetPingResponse(ctx iris.Context) error +} + +type GetPing200TextResponse StatusStrTextResponse + +func (response GetPing200TextResponse) VisitGetPingResponse(ctx iris.Context) error { + ctx.ResponseWriter().Header().Set("Content-Type", "text/plain") + ctx.StatusCode(200) + + _, err := ctx.WriteString(fmt.Sprint(response)) + return err +} + +type GetPing201TextResponse StatusIntTextResponse + +func (response GetPing201TextResponse) VisitGetPingResponse(ctx iris.Context) error { + ctx.ResponseWriter().Header().Set("Content-Type", "text/plain") + ctx.StatusCode(201) + + _, err := ctx.WriteString(fmt.Sprint(response)) + return err +} + +type GetPing202TextResponse StatusBoolTextResponse + +func (response GetPing202TextResponse) VisitGetPingResponse(ctx iris.Context) error { + ctx.ResponseWriter().Header().Set("Content-Type", "text/plain") + ctx.StatusCode(202) + + _, err := ctx.WriteString(fmt.Sprint(response)) + return err +} + +// StrictServerInterface represents all server handlers. +type StrictServerInterface interface { + + // (GET /ping) + GetPing(ctx context.Context, request GetPingRequestObject) (GetPingResponseObject, error) +} + +type StrictHandlerFunc func(ctx iris.Context, request any) (any, error) +type StrictMiddlewareFunc func(f StrictHandlerFunc, operationID string) StrictHandlerFunc + +func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface { + return &strictHandler{ssi: ssi, middlewares: middlewares} +} + +type strictHandler struct { + ssi StrictServerInterface + middlewares []StrictMiddlewareFunc +} + +// GetPing operation middleware +func (sh *strictHandler) GetPing(ctx iris.Context) { + var request GetPingRequestObject + + handler := func(ctx iris.Context, request interface{}) (interface{}, error) { + return sh.ssi.GetPing(ctx, request.(GetPingRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetPing") + } + + response, err := handler(ctx, request) + + if err != nil { + ctx.StopWithError(http.StatusInternalServerError, err) + return + } else if validResponse, ok := response.(GetPingResponseObject); ok { + if err := validResponse.VisitGetPingResponse(ctx); err != nil { + ctx.StopWithError(http.StatusInternalServerError, err) + return + } + } else if response != nil { + ctx.StopWithError(http.StatusInternalServerError, fmt.Errorf("unexpected response type: %T", response)) + return + } +} diff --git a/internal/test/bodies/content_types/text_non_string/iris/iris_test.go b/internal/test/bodies/content_types/text_non_string/iris/iris_test.go new file mode 100644 index 000000000..4024ada54 --- /dev/null +++ b/internal/test/bodies/content_types/text_non_string/iris/iris_test.go @@ -0,0 +1,32 @@ +package textnonstringiris + +import ( + "context" + "testing" + + "github.com/kataras/iris/v12" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/oapi-codegen/testutil" +) + +type server struct{} + +func (server) GetPing(_ context.Context, _ GetPingRequestObject) (GetPingResponseObject, error) { + return GetPing201TextResponse(201), nil +} + +// TestIrisIntegerTextResponse is the iris-path (ctx.WriteString) regression +// test for issue-1897: an integer text/plain response must serialize as the +// decimal "201", not the single Unicode code-point that string(response) +// previously produced. +func TestIrisIntegerTextResponse(t *testing.T) { + app := iris.New() + RegisterHandlers(app, NewStrictHandler(server{}, nil)) + + rr := testutil.NewRequest().Get("/ping").GoWithHTTPHandler(t, app).Recorder + require.Equal(t, 201, rr.Code) + assert.Equal(t, "text/plain", rr.Header().Get("Content-Type")) + assert.Equal(t, "201", rr.Body.String()) +} diff --git a/internal/test/bodies/content_types/text_non_string/spec.yaml b/internal/test/bodies/content_types/text_non_string/spec.yaml new file mode 100644 index 000000000..4da7ae998 --- /dev/null +++ b/internal/test/bodies/content_types/text_non_string/spec.yaml @@ -0,0 +1,35 @@ +openapi: 3.0.3 +info: + title: test + version: 1.0.0 +paths: + /ping: + get: + operationId: getPing + responses: + "200": + $ref: "#/components/responses/StatusStr" + "201": + $ref: "#/components/responses/StatusInt" + "202": + $ref: "#/components/responses/StatusBool" +components: + responses: + StatusStr: + description: status as string + content: + text/plain: + schema: + type: string + StatusInt: + description: status as integer + content: + text/plain: + schema: + type: integer + StatusBool: + description: status as boolean + content: + text/plain: + schema: + type: boolean diff --git a/internal/test/bodies/content_types/text_non_string/text_non_string.gen.go b/internal/test/bodies/content_types/text_non_string/text_non_string.gen.go new file mode 100644 index 000000000..a8cf260fe --- /dev/null +++ b/internal/test/bodies/content_types/text_non_string/text_non_string.gen.go @@ -0,0 +1,283 @@ +//go:build go1.22 + +// Package textnonstring provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.0.0-00010101000000-000000000000 DO NOT EDIT. +package textnonstring + +import ( + "context" + "fmt" + "net/http" +) + +// ServerInterface represents all server handlers. +type ServerInterface interface { + + // (GET /ping) + GetPing(w http.ResponseWriter, r *http.Request) +} + +// ServerInterfaceWrapper converts contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface + HandlerMiddlewares []MiddlewareFunc + ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +type MiddlewareFunc func(http.Handler) http.Handler + +// GetPing operation middleware +func (siw *ServerInterfaceWrapper) GetPing(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetPing(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +type UnescapedCookieParamError struct { + ParamName string + Err error +} + +func (e *UnescapedCookieParamError) Error() string { + return fmt.Sprintf("error unescaping cookie parameter '%s'", e.ParamName) +} + +func (e *UnescapedCookieParamError) Unwrap() error { + return e.Err +} + +type UnmarshalingParamError struct { + ParamName string + Err error +} + +func (e *UnmarshalingParamError) Error() string { + return fmt.Sprintf("Error unmarshaling parameter %s as JSON: %s", e.ParamName, e.Err.Error()) +} + +func (e *UnmarshalingParamError) Unwrap() error { + return e.Err +} + +type RequiredParamError struct { + ParamName string +} + +func (e *RequiredParamError) Error() string { + return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) +} + +type RequiredHeaderError struct { + ParamName string + Err error +} + +func (e *RequiredHeaderError) Error() string { + return fmt.Sprintf("Header parameter %s is required, but not found", e.ParamName) +} + +func (e *RequiredHeaderError) Unwrap() error { + return e.Err +} + +type InvalidParamFormatError struct { + ParamName string + Err error +} + +func (e *InvalidParamFormatError) Error() string { + return fmt.Sprintf("Invalid format for parameter %s: %s", e.ParamName, e.Err.Error()) +} + +func (e *InvalidParamFormatError) Unwrap() error { + return e.Err +} + +type TooManyValuesForParamError struct { + ParamName string + Count int +} + +func (e *TooManyValuesForParamError) Error() string { + return fmt.Sprintf("Expected one value for %s, got %d", e.ParamName, e.Count) +} + +// Handler creates http.Handler with routing matching OpenAPI spec. +func Handler(si ServerInterface) http.Handler { + return HandlerWithOptions(si, StdHTTPServerOptions{}) +} + +// ServeMux is an abstraction of [http.ServeMux]. +type ServeMux interface { + HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) + http.Handler +} + +type StdHTTPServerOptions struct { + BaseURL string + BaseRouter ServeMux + Middlewares []MiddlewareFunc + ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux. +func HandlerFromMux(si ServerInterface, m ServeMux) http.Handler { + return HandlerWithOptions(si, StdHTTPServerOptions{ + BaseRouter: m, + }) +} + +func HandlerFromMuxWithBaseURL(si ServerInterface, m ServeMux, baseURL string) http.Handler { + return HandlerWithOptions(si, StdHTTPServerOptions{ + BaseURL: baseURL, + BaseRouter: m, + }) +} + +// HandlerWithOptions creates http.Handler with additional options +func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.Handler { + m := options.BaseRouter + + if m == nil { + m = http.NewServeMux() + } + if options.ErrorHandlerFunc == nil { + options.ErrorHandlerFunc = func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusBadRequest) + } + } + + wrapper := ServerInterfaceWrapper{ + Handler: si, + HandlerMiddlewares: options.Middlewares, + ErrorHandlerFunc: options.ErrorHandlerFunc, + } + + m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/ping", wrapper.GetPing) + + return m +} + +type StatusBoolTextResponse bool + +type StatusIntTextResponse int + +type StatusStrTextResponse string + +type GetPingRequestObject struct { +} + +type GetPingResponseObject interface { + VisitGetPingResponse(w http.ResponseWriter) error +} + +type GetPing200TextResponse StatusStrTextResponse + +func (response GetPing200TextResponse) VisitGetPingResponse(w http.ResponseWriter) error { + + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(200) + + _, err := w.Write([]byte(fmt.Sprint(response))) + return err +} + +type GetPing201TextResponse StatusIntTextResponse + +func (response GetPing201TextResponse) VisitGetPingResponse(w http.ResponseWriter) error { + + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(201) + + _, err := w.Write([]byte(fmt.Sprint(response))) + return err +} + +type GetPing202TextResponse StatusBoolTextResponse + +func (response GetPing202TextResponse) VisitGetPingResponse(w http.ResponseWriter) error { + + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(202) + + _, err := w.Write([]byte(fmt.Sprint(response))) + return err +} + +// StrictServerInterface represents all server handlers. +type StrictServerInterface interface { + + // (GET /ping) + GetPing(ctx context.Context, request GetPingRequestObject) (GetPingResponseObject, error) +} + +type StrictHandlerFunc func(ctx context.Context, w http.ResponseWriter, r *http.Request, request any) (any, error) +type StrictMiddlewareFunc func(f StrictHandlerFunc, operationID string) StrictHandlerFunc + +type StrictHTTPServerOptions struct { + RequestErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) + ResponseErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface { + return &strictHandler{ssi: ssi, middlewares: middlewares, options: StrictHTTPServerOptions{ + RequestErrorHandlerFunc: func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusBadRequest) + }, + ResponseErrorHandlerFunc: func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusInternalServerError) + }, + }} +} + +func NewStrictHandlerWithOptions(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc, options StrictHTTPServerOptions) ServerInterface { + if options.RequestErrorHandlerFunc == nil { + options.RequestErrorHandlerFunc = func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusBadRequest) + } + } + if options.ResponseErrorHandlerFunc == nil { + options.ResponseErrorHandlerFunc = func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } + return &strictHandler{ssi: ssi, middlewares: middlewares, options: options} +} + +type strictHandler struct { + ssi StrictServerInterface + middlewares []StrictMiddlewareFunc + options StrictHTTPServerOptions +} + +// GetPing operation middleware +func (sh *strictHandler) GetPing(w http.ResponseWriter, r *http.Request) { + var request GetPingRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetPing(ctx, request.(GetPingRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetPing") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetPingResponseObject); ok { + if err := validResponse.VisitGetPingResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} diff --git a/internal/test/bodies/content_types/text_non_string/text_non_string_test.go b/internal/test/bodies/content_types/text_non_string/text_non_string_test.go new file mode 100644 index 000000000..3825e733a --- /dev/null +++ b/internal/test/bodies/content_types/text_non_string/text_non_string_test.go @@ -0,0 +1,42 @@ +package textnonstring + +import ( + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestIntegerTextResponse is the regression test for issue-1897: a text/plain +// response with an integer schema must write the decimal representation of the +// value, not fail to compile and not emit the Unicode code point. +func TestIntegerTextResponse(t *testing.T) { + resp := GetPing201TextResponse(201) + w := httptest.NewRecorder() + + require.NoError(t, resp.VisitGetPingResponse(w)) + assert.Equal(t, 201, w.Code) + assert.Equal(t, "text/plain", w.Header().Get("Content-Type")) + assert.Equal(t, "201", w.Body.String()) +} + +// TestBooleanTextResponse covers the other non-string-convertible primitive. +func TestBooleanTextResponse(t *testing.T) { + resp := GetPing202TextResponse(true) + w := httptest.NewRecorder() + + require.NoError(t, resp.VisitGetPingResponse(w)) + assert.Equal(t, 202, w.Code) + assert.Equal(t, "true", w.Body.String()) +} + +// TestStringTextResponse confirms the common string case is unchanged. +func TestStringTextResponse(t *testing.T) { + resp := GetPing200TextResponse("pong") + w := httptest.NewRecorder() + + require.NoError(t, resp.VisitGetPingResponse(w)) + assert.Equal(t, 200, w.Code) + assert.Equal(t, "pong", w.Body.String()) +} diff --git a/internal/test/bodies/responses/headers/headers.gen.go b/internal/test/bodies/responses/headers/headers.gen.go index 3d0004f5d..eab62e048 100644 --- a/internal/test/bodies/responses/headers/headers.gen.go +++ b/internal/test/bodies/responses/headers/headers.gen.go @@ -183,7 +183,7 @@ func (response GetPing200TextResponse) VisitGetPingResponse(w http.ResponseWrite } w.WriteHeader(200) - _, err := w.Write([]byte(response.Body)) + _, err := w.Write([]byte(fmt.Sprint(response.Body))) return err } diff --git a/internal/test/servers/strict/buffering/serversstrictbuffering.gen.go b/internal/test/servers/strict/buffering/serversstrictbuffering.gen.go index b17dd3058..77177058c 100644 --- a/internal/test/servers/strict/buffering/serversstrictbuffering.gen.go +++ b/internal/test/servers/strict/buffering/serversstrictbuffering.gen.go @@ -381,7 +381,7 @@ func (response TextEndpoint200TextResponse) VisitTextEndpointResponse(w http.Res w.Header().Set("Content-Type", "text/plain") w.WriteHeader(200) - _, err := w.Write([]byte(response)) + _, err := w.Write([]byte(fmt.Sprint(response))) return err } diff --git a/internal/test/servers/strict/chi/server.gen.go b/internal/test/servers/strict/chi/server.gen.go index 51f033f1a..18950caab 100644 --- a/internal/test/servers/strict/chi/server.gen.go +++ b/internal/test/servers/strict/chi/server.gen.go @@ -834,7 +834,7 @@ func (response MultipleRequestAndResponseTypes200TextResponse) VisitMultipleRequ w.Header().Set("Content-Type", "text/plain") w.WriteHeader(200) - _, err := w.Write([]byte(response)) + _, err := w.Write([]byte(fmt.Sprint(response))) return err } @@ -925,7 +925,7 @@ func (response RequiredTextBody200TextResponse) VisitRequiredTextBodyResponse(w w.Header().Set("Content-Type", "text/plain") w.WriteHeader(200) - _, err := w.Write([]byte(response)) + _, err := w.Write([]byte(fmt.Sprint(response))) return err } @@ -960,7 +960,7 @@ func (response ReservedGoKeywordParameters200TextResponse) VisitReservedGoKeywor w.Header().Set("Content-Type", "text/plain") w.WriteHeader(200) - _, err := w.Write([]byte(response)) + _, err := w.Write([]byte(fmt.Sprint(response))) return err } @@ -1042,7 +1042,7 @@ func (response TextExample200TextResponse) VisitTextExampleResponse(w http.Respo w.Header().Set("Content-Type", "text/plain") w.WriteHeader(200) - _, err := w.Write([]byte(response)) + _, err := w.Write([]byte(fmt.Sprint(response))) return err } diff --git a/internal/test/servers/strict/echo/server.gen.go b/internal/test/servers/strict/echo/server.gen.go index 244e40020..b8a83811e 100644 --- a/internal/test/servers/strict/echo/server.gen.go +++ b/internal/test/servers/strict/echo/server.gen.go @@ -544,7 +544,7 @@ func (response MultipleRequestAndResponseTypes200TextResponse) VisitMultipleRequ w.Header().Set("Content-Type", "text/plain") w.WriteHeader(200) - _, err := w.Write([]byte(response)) + _, err := w.Write([]byte(fmt.Sprint(response))) return err } @@ -635,7 +635,7 @@ func (response RequiredTextBody200TextResponse) VisitRequiredTextBodyResponse(w w.Header().Set("Content-Type", "text/plain") w.WriteHeader(200) - _, err := w.Write([]byte(response)) + _, err := w.Write([]byte(fmt.Sprint(response))) return err } @@ -670,7 +670,7 @@ func (response ReservedGoKeywordParameters200TextResponse) VisitReservedGoKeywor w.Header().Set("Content-Type", "text/plain") w.WriteHeader(200) - _, err := w.Write([]byte(response)) + _, err := w.Write([]byte(fmt.Sprint(response))) return err } @@ -752,7 +752,7 @@ func (response TextExample200TextResponse) VisitTextExampleResponse(w http.Respo w.Header().Set("Content-Type", "text/plain") w.WriteHeader(200) - _, err := w.Write([]byte(response)) + _, err := w.Write([]byte(fmt.Sprint(response))) return err } diff --git a/internal/test/servers/strict/echo5/server.gen.go b/internal/test/servers/strict/echo5/server.gen.go index 1a1661503..8663da31b 100644 --- a/internal/test/servers/strict/echo5/server.gen.go +++ b/internal/test/servers/strict/echo5/server.gen.go @@ -544,7 +544,7 @@ func (response MultipleRequestAndResponseTypes200TextResponse) VisitMultipleRequ w.Header().Set("Content-Type", "text/plain") w.WriteHeader(200) - _, err := w.Write([]byte(response)) + _, err := w.Write([]byte(fmt.Sprint(response))) return err } @@ -635,7 +635,7 @@ func (response RequiredTextBody200TextResponse) VisitRequiredTextBodyResponse(w w.Header().Set("Content-Type", "text/plain") w.WriteHeader(200) - _, err := w.Write([]byte(response)) + _, err := w.Write([]byte(fmt.Sprint(response))) return err } @@ -670,7 +670,7 @@ func (response ReservedGoKeywordParameters200TextResponse) VisitReservedGoKeywor w.Header().Set("Content-Type", "text/plain") w.WriteHeader(200) - _, err := w.Write([]byte(response)) + _, err := w.Write([]byte(fmt.Sprint(response))) return err } @@ -752,7 +752,7 @@ func (response TextExample200TextResponse) VisitTextExampleResponse(w http.Respo w.Header().Set("Content-Type", "text/plain") w.WriteHeader(200) - _, err := w.Write([]byte(response)) + _, err := w.Write([]byte(fmt.Sprint(response))) return err } diff --git a/internal/test/servers/strict/fiber/server.gen.go b/internal/test/servers/strict/fiber/server.gen.go index 19532d2db..7f0117a35 100644 --- a/internal/test/servers/strict/fiber/server.gen.go +++ b/internal/test/servers/strict/fiber/server.gen.go @@ -682,7 +682,7 @@ func (response MultipleRequestAndResponseTypes200TextResponse) VisitMultipleRequ ctx.Response().Header.Set("Content-Type", "text/plain") ctx.Status(200) - _, err := ctx.WriteString(string(response)) + _, err := ctx.WriteString(fmt.Sprint(response)) return err } @@ -767,7 +767,7 @@ func (response RequiredTextBody200TextResponse) VisitRequiredTextBodyResponse(ct ctx.Response().Header.Set("Content-Type", "text/plain") ctx.Status(200) - _, err := ctx.WriteString(string(response)) + _, err := ctx.WriteString(fmt.Sprint(response)) return err } @@ -801,7 +801,7 @@ func (response ReservedGoKeywordParameters200TextResponse) VisitReservedGoKeywor ctx.Response().Header.Set("Content-Type", "text/plain") ctx.Status(200) - _, err := ctx.WriteString(string(response)) + _, err := ctx.WriteString(fmt.Sprint(response)) return err } @@ -872,7 +872,7 @@ func (response TextExample200TextResponse) VisitTextExampleResponse(ctx *fiber.C ctx.Response().Header.Set("Content-Type", "text/plain") ctx.Status(200) - _, err := ctx.WriteString(string(response)) + _, err := ctx.WriteString(fmt.Sprint(response)) return err } diff --git a/internal/test/servers/strict/gin/server.gen.go b/internal/test/servers/strict/gin/server.gen.go index a3bf62f60..3c88b4a96 100644 --- a/internal/test/servers/strict/gin/server.gen.go +++ b/internal/test/servers/strict/gin/server.gen.go @@ -613,7 +613,7 @@ func (response MultipleRequestAndResponseTypes200TextResponse) VisitMultipleRequ w.Header().Set("Content-Type", "text/plain") w.WriteHeader(200) - _, err := w.Write([]byte(response)) + _, err := w.Write([]byte(fmt.Sprint(response))) return err } @@ -704,7 +704,7 @@ func (response RequiredTextBody200TextResponse) VisitRequiredTextBodyResponse(w w.Header().Set("Content-Type", "text/plain") w.WriteHeader(200) - _, err := w.Write([]byte(response)) + _, err := w.Write([]byte(fmt.Sprint(response))) return err } @@ -739,7 +739,7 @@ func (response ReservedGoKeywordParameters200TextResponse) VisitReservedGoKeywor w.Header().Set("Content-Type", "text/plain") w.WriteHeader(200) - _, err := w.Write([]byte(response)) + _, err := w.Write([]byte(fmt.Sprint(response))) return err } @@ -821,7 +821,7 @@ func (response TextExample200TextResponse) VisitTextExampleResponse(w http.Respo w.Header().Set("Content-Type", "text/plain") w.WriteHeader(200) - _, err := w.Write([]byte(response)) + _, err := w.Write([]byte(fmt.Sprint(response))) return err } diff --git a/internal/test/servers/strict/gorilla/server.gen.go b/internal/test/servers/strict/gorilla/server.gen.go index 88bde31ec..a2628d898 100644 --- a/internal/test/servers/strict/gorilla/server.gen.go +++ b/internal/test/servers/strict/gorilla/server.gen.go @@ -733,7 +733,7 @@ func (response MultipleRequestAndResponseTypes200TextResponse) VisitMultipleRequ w.Header().Set("Content-Type", "text/plain") w.WriteHeader(200) - _, err := w.Write([]byte(response)) + _, err := w.Write([]byte(fmt.Sprint(response))) return err } @@ -824,7 +824,7 @@ func (response RequiredTextBody200TextResponse) VisitRequiredTextBodyResponse(w w.Header().Set("Content-Type", "text/plain") w.WriteHeader(200) - _, err := w.Write([]byte(response)) + _, err := w.Write([]byte(fmt.Sprint(response))) return err } @@ -859,7 +859,7 @@ func (response ReservedGoKeywordParameters200TextResponse) VisitReservedGoKeywor w.Header().Set("Content-Type", "text/plain") w.WriteHeader(200) - _, err := w.Write([]byte(response)) + _, err := w.Write([]byte(fmt.Sprint(response))) return err } @@ -941,7 +941,7 @@ func (response TextExample200TextResponse) VisitTextExampleResponse(w http.Respo w.Header().Set("Content-Type", "text/plain") w.WriteHeader(200) - _, err := w.Write([]byte(response)) + _, err := w.Write([]byte(fmt.Sprint(response))) return err } diff --git a/internal/test/servers/strict/iris/server.gen.go b/internal/test/servers/strict/iris/server.gen.go index 334f95d98..8090d5d81 100644 --- a/internal/test/servers/strict/iris/server.gen.go +++ b/internal/test/servers/strict/iris/server.gen.go @@ -498,7 +498,7 @@ func (response MultipleRequestAndResponseTypes200TextResponse) VisitMultipleRequ ctx.ResponseWriter().Header().Set("Content-Type", "text/plain") ctx.StatusCode(200) - _, err := ctx.WriteString(string(response)) + _, err := ctx.WriteString(fmt.Sprint(response)) return err } @@ -583,7 +583,7 @@ func (response RequiredTextBody200TextResponse) VisitRequiredTextBodyResponse(ct ctx.ResponseWriter().Header().Set("Content-Type", "text/plain") ctx.StatusCode(200) - _, err := ctx.WriteString(string(response)) + _, err := ctx.WriteString(fmt.Sprint(response)) return err } @@ -617,7 +617,7 @@ func (response ReservedGoKeywordParameters200TextResponse) VisitReservedGoKeywor ctx.ResponseWriter().Header().Set("Content-Type", "text/plain") ctx.StatusCode(200) - _, err := ctx.WriteString(string(response)) + _, err := ctx.WriteString(fmt.Sprint(response)) return err } @@ -688,7 +688,7 @@ func (response TextExample200TextResponse) VisitTextExampleResponse(ctx iris.Con ctx.ResponseWriter().Header().Set("Content-Type", "text/plain") ctx.StatusCode(200) - _, err := ctx.WriteString(string(response)) + _, err := ctx.WriteString(fmt.Sprint(response)) return err } diff --git a/internal/test/servers/strict/stdhttp/server.gen.go b/internal/test/servers/strict/stdhttp/server.gen.go index 516eae31a..bf2ea86fa 100644 --- a/internal/test/servers/strict/stdhttp/server.gen.go +++ b/internal/test/servers/strict/stdhttp/server.gen.go @@ -726,7 +726,7 @@ func (response MultipleRequestAndResponseTypes200TextResponse) VisitMultipleRequ w.Header().Set("Content-Type", "text/plain") w.WriteHeader(200) - _, err := w.Write([]byte(response)) + _, err := w.Write([]byte(fmt.Sprint(response))) return err } @@ -817,7 +817,7 @@ func (response RequiredTextBody200TextResponse) VisitRequiredTextBodyResponse(w w.Header().Set("Content-Type", "text/plain") w.WriteHeader(200) - _, err := w.Write([]byte(response)) + _, err := w.Write([]byte(fmt.Sprint(response))) return err } @@ -852,7 +852,7 @@ func (response ReservedGoKeywordParameters200TextResponse) VisitReservedGoKeywor w.Header().Set("Content-Type", "text/plain") w.WriteHeader(200) - _, err := w.Write([]byte(response)) + _, err := w.Write([]byte(fmt.Sprint(response))) return err } @@ -934,7 +934,7 @@ func (response TextExample200TextResponse) VisitTextExampleResponse(w http.Respo w.Header().Set("Content-Type", "text/plain") w.WriteHeader(200) - _, err := w.Write([]byte(response)) + _, err := w.Write([]byte(fmt.Sprint(response))) return err } diff --git a/pkg/codegen/templates/strict/strict-fiber-interface.tmpl b/pkg/codegen/templates/strict/strict-fiber-interface.tmpl index 4deabb486..98ce54271 100644 --- a/pkg/codegen/templates/strict/strict-fiber-interface.tmpl +++ b/pkg/codegen/templates/strict/strict-fiber-interface.tmpl @@ -108,7 +108,7 @@ {{$hasUnionElements := ne 0 (len .Schema.UnionElements)}} return ctx.JSON(&{{if $hasBodyVar}}response.Body{{else}}response{{end}}{{if and $hasUnionElements (not .Schema.IsExternalRef)}}.union{{end}}) {{else if eq .NameTag "Text" -}} - _, err := ctx.WriteString(string({{if $hasBodyVar}}response.Body{{else}}response{{end}})) + _, err := ctx.WriteString(fmt.Sprint({{if $hasBodyVar}}response.Body{{else}}response{{end}})) return err {{else if eq .NameTag "Formdata" -}} if form, err := runtime.MarshalForm({{if $hasBodyVar}}response.Body{{else}}response{{end}}, nil); err != nil { diff --git a/pkg/codegen/templates/strict/strict-fiber-v3-interface.tmpl b/pkg/codegen/templates/strict/strict-fiber-v3-interface.tmpl index 6806c0ea8..b93c5d0f0 100644 --- a/pkg/codegen/templates/strict/strict-fiber-v3-interface.tmpl +++ b/pkg/codegen/templates/strict/strict-fiber-v3-interface.tmpl @@ -108,7 +108,7 @@ {{$hasUnionElements := ne 0 (len .Schema.UnionElements)}} return ctx.JSON(&{{if $hasBodyVar}}response.Body{{else}}response{{end}}{{if and $hasUnionElements (not .Schema.IsExternalRef)}}.union{{end}}) {{else if eq .NameTag "Text" -}} - _, err := ctx.WriteString(string({{if $hasBodyVar}}response.Body{{else}}response{{end}})) + _, err := ctx.WriteString(fmt.Sprint({{if $hasBodyVar}}response.Body{{else}}response{{end}})) return err {{else if eq .NameTag "Formdata" -}} if form, err := runtime.MarshalForm({{if $hasBodyVar}}response.Body{{else}}response{{end}}, nil); err != nil { diff --git a/pkg/codegen/templates/strict/strict-interface.tmpl b/pkg/codegen/templates/strict/strict-interface.tmpl index 081f3fb1a..1431ac9c0 100644 --- a/pkg/codegen/templates/strict/strict-interface.tmpl +++ b/pkg/codegen/templates/strict/strict-interface.tmpl @@ -152,7 +152,7 @@ w.WriteHeader({{if $fixedStatusCode}}{{$statusCode}}{{else}}response.StatusCode{{end}}) {{if eq .NameTag "Text" -}} - _, err := w.Write([]byte({{if $hasBodyVar}}response.Body{{else}}response{{end}})) + _, err := w.Write([]byte(fmt.Sprint({{if $hasBodyVar}}response.Body{{else}}response{{end}}))) return err {{else if eq .NameTag "Multipart" -}} defer writer.Close() diff --git a/pkg/codegen/templates/strict/strict-iris-interface.tmpl b/pkg/codegen/templates/strict/strict-iris-interface.tmpl index 77e5c3488..77dffa860 100644 --- a/pkg/codegen/templates/strict/strict-iris-interface.tmpl +++ b/pkg/codegen/templates/strict/strict-iris-interface.tmpl @@ -108,7 +108,7 @@ {{$hasUnionElements := ne 0 (len .Schema.UnionElements)}} return ctx.JSON(&{{if $hasBodyVar}}response.Body{{else}}response{{end}}{{if and $hasUnionElements (not .Schema.IsExternalRef)}}.union{{end}}) {{else if eq .NameTag "Text" -}} - _, err := ctx.WriteString(string({{if $hasBodyVar}}response.Body{{else}}response{{end}})) + _, err := ctx.WriteString(fmt.Sprint({{if $hasBodyVar}}response.Body{{else}}response{{end}})) return err {{else if eq .NameTag "Formdata" -}} if form, err := runtime.MarshalForm({{if $hasBodyVar}}response.Body{{else}}response{{end}}, nil); err != nil {