diff --git a/ctx.go b/ctx.go index f5f7b18f..797d6335 100644 --- a/ctx.go +++ b/ctx.go @@ -263,11 +263,11 @@ func (c *netHttpContext[B]) Body() (B, error) { } // Serialize serializes the given data to the response. It uses the Content-Type header to determine the serialization format. -func (c netHttpContext[B]) Serialize(data any) error { +func (c netHttpContext[B]) Serialize(code int, data any) error { if c.serializer == nil { - return Send(c.Res, c.Req, data) + return Send(c.Res, c.Req, code, data) } - return c.serializer(c.Res, c.Req, data) + return c.serializer(c.Res, c.Req, code, data) } // SerializeError serializes the given error to the response. It uses the Content-Type header to determine the serialization format. @@ -279,13 +279,6 @@ func (c netHttpContext[B]) SerializeError(err error) { c.errorSerializer(c.Res, c.Req, err) } -// SetDefaultStatusCode sets the default status code of the response. -func (c netHttpContext[B]) SetDefaultStatusCode() { - if c.DefaultStatusCode != 0 { - c.SetStatus(c.DefaultStatusCode) - } -} - func body[B any](c netHttpContext[B]) (B, error) { // Limit the size of the request body. if c.readOptions.MaxBodySize != 0 { diff --git a/extra/fuegoecho/context.go b/extra/fuegoecho/context.go index a21862fa..d0ba3c5e 100644 --- a/extra/fuegoecho/context.go +++ b/extra/fuegoecho/context.go @@ -102,13 +102,10 @@ func (c echoContext[B]) SetStatus(code int) { c.echoCtx.Response().WriteHeader(code) } -func (c echoContext[B]) Serialize(data any) error { +func (c echoContext[B]) Serialize(code int, data any) error { status := c.echoCtx.Response().Status if status == 0 { - status = c.DefaultStatusCode - } - if status == 0 { - status = http.StatusOK + status = code } c.echoCtx.JSON(status, data) return nil diff --git a/extra/fuegoecho/go.mod b/extra/fuegoecho/go.mod index 2b113a59..19481e29 100644 --- a/extra/fuegoecho/go.mod +++ b/extra/fuegoecho/go.mod @@ -2,6 +2,8 @@ module github.com/go-fuego/fuego/extra/fuegoecho go 1.23.3 +replace github.com/go-fuego/fuego => ../.. + require ( github.com/go-fuego/fuego v0.18.0-rc2 github.com/labstack/echo/v4 v4.13.3 diff --git a/extra/fuegoecho/go.sum b/extra/fuegoecho/go.sum index 3b35a62f..1b48aac4 100644 --- a/extra/fuegoecho/go.sum +++ b/extra/fuegoecho/go.sum @@ -4,8 +4,6 @@ github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3G github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/getkin/kin-openapi v0.128.0 h1:jqq3D9vC9pPq1dGcOCv7yOp1DaEe7c/T1vzcLbITSp4= github.com/getkin/kin-openapi v0.128.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM= -github.com/go-fuego/fuego v0.18.0-rc2 h1:Ewm8+r+/B9Lr2FKLK9rFR2fxHRdjMQ/AaGkfZgBzv4s= -github.com/go-fuego/fuego v0.18.0-rc2/go.mod h1:OY7FkKD5g154J3mCTpY6SXqBQYrcI2Rg9N0Owft4WmU= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= diff --git a/extra/fuegogin/context.go b/extra/fuegogin/context.go index c4e8afb1..53cd5067 100644 --- a/extra/fuegogin/context.go +++ b/extra/fuegogin/context.go @@ -102,13 +102,10 @@ func (c ginContext[B]) SetStatus(code int) { c.ginCtx.Status(code) } -func (c ginContext[B]) Serialize(data any) error { +func (c ginContext[B]) Serialize(code int, data any) error { status := c.ginCtx.Writer.Status() if status == 0 { - status = c.DefaultStatusCode - } - if status == 0 { - status = http.StatusOK + status = code } c.ginCtx.JSON(status, data) return nil @@ -122,10 +119,3 @@ func (c ginContext[B]) SerializeError(err error) { } c.ginCtx.JSON(statusCode, err) } - -func (c ginContext[B]) SetDefaultStatusCode() { - if c.DefaultStatusCode == 0 { - c.DefaultStatusCode = http.StatusOK - } - c.SetStatus(c.DefaultStatusCode) -} diff --git a/extra/fuegogin/go.mod b/extra/fuegogin/go.mod index acb3b108..4b8decb1 100644 --- a/extra/fuegogin/go.mod +++ b/extra/fuegogin/go.mod @@ -2,6 +2,8 @@ module github.com/go-fuego/fuego/extra/fuegogin go 1.23.3 +replace github.com/go-fuego/fuego => ../.. + require ( github.com/gin-gonic/gin v1.10.0 github.com/go-fuego/fuego v0.17.0 @@ -21,6 +23,7 @@ require ( github.com/go-playground/validator/v10 v10.23.0 // indirect github.com/goccy/go-json v0.10.4 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/gorilla/schema v1.4.1 // indirect github.com/invopop/yaml v0.3.1 // indirect github.com/josharian/intern v1.0.0 // indirect diff --git a/extra/fuegogin/go.sum b/extra/fuegogin/go.sum index 3b0771f8..ea398d34 100644 --- a/extra/fuegogin/go.sum +++ b/extra/fuegogin/go.sum @@ -17,8 +17,6 @@ github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= -github.com/go-fuego/fuego v0.17.0 h1:UBR0ib0Qq0XkGX8J3OTIwrlvHDGmysBVG3NHowA6UCQ= -github.com/go-fuego/fuego v0.17.0/go.mod h1:glaJIBAO3AaZx+c4jGYW4pHyhQkeMGuHx8qLchGAH8M= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= @@ -37,9 +35,11 @@ github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E= github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= @@ -106,6 +106,8 @@ golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU= google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/common_context.go b/internal/common_context.go index 9343968b..2e0f84ae 100644 --- a/internal/common_context.go +++ b/internal/common_context.go @@ -43,6 +43,10 @@ type CommonContext[B any] struct { DefaultStatusCode int } +func (c CommonContext[B]) GetDefaultStatusCode() int { + return c.DefaultStatusCode +} + type ParamType string // Query, Header, Cookie // GetOpenAPIParams returns the OpenAPI parameters declared in the OpenAPI spec. diff --git a/serialization.go b/serialization.go index b05e21e0..8c04f15e 100644 --- a/serialization.go +++ b/serialization.go @@ -79,25 +79,31 @@ func transformOut[T any](ctx context.Context, ans T) (T, error) { return ans, nil } -type Sender func(http.ResponseWriter, *http.Request, any) error +type Sender func(http.ResponseWriter, *http.Request, int, any) error +type internalSender func(http.ResponseWriter, *http.Request, any) error // Send sends a response. // The format is determined by the Accept header. // If Accept header `*/*` is found Send will Attempt to send // HTML, and then JSON. -func Send(w http.ResponseWriter, r *http.Request, ans any) (err error) { +func Send(w http.ResponseWriter, r *http.Request, code int, ans any) (err error) { + send := func(mimeType string, code int, ans any, sender internalSender) error { + w.Header().Set("Content-Type", mimeType) + w.WriteHeader(code) + return sender(w, r, ans) + } for _, header := range parseAcceptHeader(r.Header) { - switch inferAcceptHeader(header, ans) { + switch header := inferAcceptHeader(header, ans); header { case "application/xml": - err = SendXML(w, nil, ans) + err = send("application/xml", code, ans, SendXML) case "text/html": - err = SendHTML(w, r, ans) + err = send("text/html; charset=utf-8", code, ans, SendHTML) case "text/plain": - err = SendText(w, nil, ans) + err = send("text/plain; charset=utf-8", code, ans, SendText) case "application/json": - err = SendJSON(w, nil, ans) + err = send("application/json", code, ans, SendJSON) case "application/x-yaml", "text/yaml; charset=utf-8", "application/yaml": // https://www.rfc-editor.org/rfc/rfc9512.html - err = SendYAML(w, nil, ans) + err = send("application/x-yaml", code, ans, SendYAML) default: // if we don't support the header, try the next one continue @@ -128,7 +134,6 @@ var SendYAML = func(w http.ResponseWriter, _ *http.Request, ans any) (err error) } }() - w.Header().Set("Content-Type", "application/x-yaml") err = yaml.NewEncoder(w).Encode(ans) if err != nil { w.WriteHeader(http.StatusInternalServerError) @@ -155,7 +160,6 @@ func SendYAMLError(w http.ResponseWriter, _ *http.Request, err error) { // Declared as a variable to be able to override it for clients that need to customize serialization. // If serialization fails, it does NOT write to the response writer. It has to be passed to SendJSONError. var SendJSON = func(w http.ResponseWriter, _ *http.Request, ans any) error { - w.Header().Set("Content-Type", "application/json") err := json.NewEncoder(w).Encode(ans) if err != nil { slog.Error("Cannot serialize returned response to JSON", "error", err, "errtype", fmt.Sprintf("%T", err)) @@ -219,7 +223,6 @@ func SendJSONError(w http.ResponseWriter, _ *http.Request, err error) { // Declared as a variable to be able to override it for clients that need to customize serialization. // If serialization fails, it does NOT write to the response writer. It has to be passed to SendJSONError. var SendXML = func(w http.ResponseWriter, _ *http.Request, ans any) error { - w.Header().Set("Content-Type", "application/xml") err := xml.NewEncoder(w).Encode(ans) if err != nil { slog.Error("Cannot serialize returned response to XML", "error", err, "errtype", fmt.Sprintf("%T", err)) @@ -255,8 +258,6 @@ func SendXMLError(w http.ResponseWriter, _ *http.Request, err error) { // SendHTML sends a HTML response. // Declared as a variable to be able to override it for clients that need to customize serialization. var SendHTML = func(w http.ResponseWriter, r *http.Request, ans any) error { - w.Header().Set("Content-Type", "text/html; charset=utf-8") - ctxRenderer, ok := any(ans).(CtxRenderer) if ok { return ctxRenderer.Render(r.Context(), w) @@ -299,8 +300,6 @@ func SendHTMLError(w http.ResponseWriter, _ *http.Request, err error) { // SendText sends a HTML response. // Declared as a variable to be able to override it for clients that need to customize serialization. func SendText(w http.ResponseWriter, _ *http.Request, ans any) error { - var err error - w.Header().Set("Content-Type", "text/plain; charset=utf-8") stringToWrite, ok := any(ans).(string) if !ok { stringToWritePtr, okPtr := any(ans).(*string) @@ -310,8 +309,7 @@ func SendText(w http.ResponseWriter, _ *http.Request, ans any) error { stringToWrite = fmt.Sprintf("%v", ans) } } - _, err = w.Write([]byte(stringToWrite)) - + _, err := w.Write([]byte(stringToWrite)) return err } diff --git a/serialization_test.go b/serialization_test.go index fdcf00ce..0a242490 100644 --- a/serialization_test.go +++ b/serialization_test.go @@ -30,7 +30,7 @@ func TestSend(t *testing.T) { w := httptest.NewRecorder() r := httptest.NewRequest(http.MethodGet, "/", nil) r.Header.Set("Accept", "application/json") - Send(w, r, &StdRenderer{ + Send(w, r, 200, &StdRenderer{ templates: template, templateToExecute: templateName, }) @@ -44,7 +44,7 @@ func TestSendWhenError(t *testing.T) { r := httptest.NewRequest(http.MethodGet, "/", nil) r.Header.Set("Accept", "text/junk,application/json,text/html") errorWriter := &errorWriter{} - err := Send(errorWriter, r, response{}) + err := Send(errorWriter, r, 0, response{}) require.Error(t, err) SendError(w, r, err) require.Equal(t, "application/json", w.Header().Get("Content-Type")) diff --git a/serve.go b/serve.go index a8a9a57b..4acab801 100644 --- a/serve.go +++ b/serve.go @@ -106,10 +106,9 @@ func HTTPHandler[ReturnType, Body any](s *Server, controller func(c ContextWithB type ContextFlowable[B any] interface { ContextWithBody[B] - // SetDefaultStatusCode sets the status code of the response defined in the options. - SetDefaultStatusCode() + GetDefaultStatusCode() int // Serialize serializes the given data to the response. - Serialize(data any) error + Serialize(code int, data any) error // SerializeError serializes the given error to the response. SerializeError(err error) } @@ -141,8 +140,6 @@ func Flow[B, T any](s *Engine, ctx ContextFlowable[B], controller func(c Context } ctx.SetHeader("Server-Timing", Timing{"controller", "", time.Since(timeController)}.String()) - ctx.SetDefaultStatusCode() - if reflect.TypeOf(ans) == nil { return } @@ -158,8 +155,12 @@ func Flow[B, T any](s *Engine, ctx ContextFlowable[B], controller func(c Context timeAfterTransformOut := time.Now() ctx.SetHeader("Server-Timing", Timing{"transformOut", "transformOut", timeAfterTransformOut.Sub(timeTransformOut)}.String()) + code := ctx.GetDefaultStatusCode() + if code == 0 { + code = http.StatusOK + } // SERIALIZATION - err = ctx.Serialize(ans) + err = ctx.Serialize(code, ans) if err != nil { err = s.ErrorHandler(err) ctx.SerializeError(err) diff --git a/server.go b/server.go index c40773a9..60be035b 100644 --- a/server.go +++ b/server.go @@ -335,16 +335,6 @@ func WithAddr(addr string) func(*Server) { } } -// WithXML sets the serializer to XML -// -// Deprecated: fuego supports automatic XML serialization when using the header "Accept: application/xml". -func WithXML() func(*Server) { - return func(c *Server) { - c.Serialize = SendXML - c.SerializeError = SendXMLError - } -} - // WithLogHandler sets the log handler of the server. func WithLogHandler(handler slog.Handler) func(*Server) { return func(*Server) { diff --git a/server_test.go b/server_test.go index a0af3ccc..19820333 100644 --- a/server_test.go +++ b/server_test.go @@ -322,7 +322,7 @@ func dummyController(_ ContextWithBody[ReqBody]) (Resp, error) { func TestCustomSerialization(t *testing.T) { s := NewServer( - WithSerializer(func(w http.ResponseWriter, r *http.Request, a any) error { + WithSerializer(func(w http.ResponseWriter, r *http.Request, _ int, a any) error { w.WriteHeader(202) _, err := w.Write([]byte("custom serialization")) return err