From 4402df727aebeb4532aa9bc476db6fee22beeb75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Muhammed=20Efe=20=C3=87etin?= Date: Mon, 8 Aug 2022 12:46:54 +0300 Subject: [PATCH 1/2] :sparkles: feature: add XML to context. --- app.go | 70 +++++++++++++++++++++++++++++------------------ ctx.go | 19 +++++++++---- ctx_test.go | 60 ++++++++++++++++++++++++++++++++++++++++ utils/xml.go | 4 +++ utils/xml_test.go | 59 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 179 insertions(+), 33 deletions(-) create mode 100644 utils/xml.go create mode 100644 utils/xml_test.go diff --git a/app.go b/app.go index ae922aa007..31039ce4d7 100644 --- a/app.go +++ b/app.go @@ -23,6 +23,7 @@ import ( "time" "encoding/json" + "encoding/xml" "github.com/gofiber/fiber/v2/utils" "github.com/valyala/fasthttp" @@ -63,17 +64,18 @@ type Storage interface { // ErrorHandler defines a function that will process all errors // returned from any handlers in the stack -// cfg := fiber.Config{} -// cfg.ErrorHandler = func(c *Ctx, err error) error { -// code := StatusInternalServerError -// var e *fiber.Error -// if errors.As(err, &e) { -// code = e.Code -// } -// c.Set(HeaderContentType, MIMETextPlainCharsetUTF8) -// return c.Status(code).SendString(err.Error()) -// } -// app := fiber.New(cfg) +// +// cfg := fiber.Config{} +// cfg.ErrorHandler = func(c *Ctx, err error) error { +// code := StatusInternalServerError +// var e *fiber.Error +// if errors.As(err, &e) { +// code = e.Code +// } +// c.Set(HeaderContentType, MIMETextPlainCharsetUTF8) +// return c.Status(code).SendString(err.Error()) +// } +// app := fiber.New(cfg) type ErrorHandler = func(*Ctx, error) error // Error represents an error that occurred while handling a request. @@ -323,6 +325,13 @@ type Config struct { // Default: json.Unmarshal JSONDecoder utils.JSONUnmarshal `json:"-"` + // XMLEncoder set by an external client of Fiber it will use the provided implementation of a + // XMLMarshal + // + // Allowing for flexibility in using another XML library for encoding + // Default: xml.Marshal + XMLEncoder utils.JSONMarshal `json:"-"` + // Known networks are "tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only) // WARNING: When prefork is set to true, only "tcp4" and "tcp6" can be chose. // @@ -438,12 +447,15 @@ var DefaultErrorHandler = func(c *Ctx, err error) error { } // New creates a new Fiber named instance. -// app := fiber.New() +// +// app := fiber.New() +// // You can pass optional configuration options by passing a Config struct: -// app := fiber.New(fiber.Config{ -// Prefork: true, -// ServerHeader: "Fiber", -// }) +// +// app := fiber.New(fiber.Config{ +// Prefork: true, +// ServerHeader: "Fiber", +// }) func New(config ...Config) *App { // Create a new app app := &App{ @@ -509,6 +521,9 @@ func New(config ...Config) *App { if app.config.JSONDecoder == nil { app.config.JSONDecoder = json.Unmarshal } + if app.config.XMLEncoder == nil { + app.config.XMLEncoder = xml.Marshal + } if app.config.Network == "" { app.config.Network = NetworkTCP4 } @@ -609,15 +624,15 @@ func (app *App) GetRoute(name string) Route { // Use registers a middleware route that will match requests // with the provided prefix (which is optional and defaults to "/"). // -// app.Use(func(c *fiber.Ctx) error { -// return c.Next() -// }) -// app.Use("/api", func(c *fiber.Ctx) error { -// return c.Next() -// }) -// app.Use("/api", handler, func(c *fiber.Ctx) error { -// return c.Next() -// }) +// app.Use(func(c *fiber.Ctx) error { +// return c.Next() +// }) +// app.Use("/api", func(c *fiber.Ctx) error { +// return c.Next() +// }) +// app.Use("/api", handler, func(c *fiber.Ctx) error { +// return c.Next() +// }) // // This method will match all HTTP verbs: GET, POST, PUT, HEAD etc... func (app *App) Use(args ...interface{}) Router { @@ -710,8 +725,9 @@ func (app *App) All(path string, handlers ...Handler) Router { } // Group is used for Routes with common prefix to define a new sub-router with optional middleware. -// api := app.Group("/api") -// api.Get("/users", handler) +// +// api := app.Group("/api") +// api.Get("/users", handler) func (app *App) Group(prefix string, handlers ...Handler) Router { if len(handlers) > 0 { app.register(methodUse, prefix, handlers...) diff --git a/ctx.go b/ctx.go index 004cf3ec7a..30aed27682 100644 --- a/ctx.go +++ b/ctx.go @@ -518,12 +518,7 @@ func (c *Ctx) Format(body interface{}) error { case "txt": return c.SendString(b) case "xml": - raw, err := xml.Marshal(body) - if err != nil { - return fmt.Errorf("error serializing xml: %v", body) - } - c.fasthttp.Response.SetBody(raw) - return nil + return c.XML(body) } return c.SendString(b) } @@ -736,6 +731,18 @@ func (c *Ctx) JSONP(data interface{}, callback ...string) error { return c.SendString(result) } +// XML converts any interface or string to XML. +// This method also sets the content header to application/xml. +func (c *Ctx) XML(data interface{}) error { + raw, err := c.app.config.XMLEncoder(data) + if err != nil { + return err + } + c.fasthttp.Response.SetBodyRaw(raw) + c.fasthttp.Response.Header.SetContentType(MIMEApplicationXML) + return nil +} + // Links joins the links followed by the property to populate the response's Link HTTP header field. func (c *Ctx) Links(link ...string) { if len(link) == 0 { diff --git a/ctx_test.go b/ctx_test.go index 325b67d78b..3af6e806b0 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -12,6 +12,7 @@ import ( "bytes" "compress/gzip" "context" + "encoding/xml" "errors" "fmt" "io" @@ -2134,6 +2135,65 @@ func Benchmark_Ctx_JSONP(b *testing.B) { utils.AssertEqual(b, `emit({"Name":"Grame","Age":20});`, string(c.Response().Body())) } +// go test -run Test_Ctx_XML +func Test_Ctx_XML(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + utils.AssertEqual(t, true, c.JSON(complex(1, 1)) != nil) + + type xmlResult struct { + XMLName xml.Name `xml:"Users"` + Names []string `xml:"Names"` + Ages []int `xml:"Ages"` + } + + c.XML(xmlResult{ + Names: []string{"Grame", "John"}, + Ages: []int{1, 12, 20}, + }) + + utils.AssertEqual(t, `GrameJohn11220`, string(c.Response().Body())) + utils.AssertEqual(t, "application/xml", string(c.Response().Header.Peek("content-type"))) + + testEmpty := func(v interface{}, r string) { + err := c.XML(v) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, r, string(c.Response().Body())) + } + + testEmpty(nil, "") + testEmpty("", ``) + testEmpty(0, "0") + testEmpty([]int{}, "") +} + +// go test -run=^$ -bench=Benchmark_Ctx_XML -benchmem -count=4 +func Benchmark_Ctx_XML(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type SomeStruct struct { + Name string `xml:"Name"` + Age uint8 `xml:"Age"` + } + data := SomeStruct{ + Name: "Grame", + Age: 20, + } + var err error + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + err = c.XML(data) + } + + utils.AssertEqual(b, nil, err) + utils.AssertEqual(b, `Grame20`, string(c.Response().Body())) +} + // go test -run Test_Ctx_Links func Test_Ctx_Links(t *testing.T) { t.Parallel() diff --git a/utils/xml.go b/utils/xml.go new file mode 100644 index 0000000000..cc6a024a3b --- /dev/null +++ b/utils/xml.go @@ -0,0 +1,4 @@ +package utils + +// XMLMarshal returns the XML encoding of v. +type XMLMarshal func(v interface{}) ([]byte, error) diff --git a/utils/xml_test.go b/utils/xml_test.go new file mode 100644 index 0000000000..bbb11708d6 --- /dev/null +++ b/utils/xml_test.go @@ -0,0 +1,59 @@ +package utils + +import ( + "encoding/xml" + "testing" +) + +type serversXMLStructure struct { + XMLName xml.Name `xml:"servers"` + Version string `xml:"version,attr"` + Servers []serverXMLStructure `xml:"server"` +} + +type serverXMLStructure struct { + XMLName xml.Name `xml:"server"` + Name string `xml:"name"` +} + +var xmlString = `fiber onefiber two` + +func Test_GolangXMLEncoder(t *testing.T) { + t.Parallel() + + var ( + ss = &serversXMLStructure{ + Version: "1", + Servers: []serverXMLStructure{ + {Name: "fiber one"}, + {Name: "fiber two"}, + }, + } + xmlEncoder XMLMarshal = xml.Marshal + ) + + raw, err := xmlEncoder(ss) + AssertEqual(t, err, nil) + + AssertEqual(t, string(raw), xmlString) +} + +func Test_DefaultXMLEncoder(t *testing.T) { + t.Parallel() + + var ( + ss = &serversXMLStructure{ + Version: "1", + Servers: []serverXMLStructure{ + {Name: "fiber one"}, + {Name: "fiber two"}, + }, + } + xmlEncoder XMLMarshal = xml.Marshal + ) + + raw, err := xmlEncoder(ss) + AssertEqual(t, err, nil) + + AssertEqual(t, string(raw), xmlString) +} From c5644a8fbedcb28dbd3d675b6ac0f984759a679b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=2E=20Efe=20=C3=87etin?= Date: Mon, 15 Aug 2022 15:08:19 +0300 Subject: [PATCH 2/2] Update app.go --- app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.go b/app.go index 31039ce4d7..c28aeb6389 100644 --- a/app.go +++ b/app.go @@ -330,7 +330,7 @@ type Config struct { // // Allowing for flexibility in using another XML library for encoding // Default: xml.Marshal - XMLEncoder utils.JSONMarshal `json:"-"` + XMLEncoder utils.XMLMarshal `json:"-"` // Known networks are "tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only) // WARNING: When prefork is set to true, only "tcp4" and "tcp6" can be chose.