Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

📝 [v3 Proposal]: Parity for res.format and Ctx.Format #2745

Closed
3 tasks done
nickajacks1 opened this issue Nov 30, 2023 · 4 comments · Fixed by #2766
Closed
3 tasks done

📝 [v3 Proposal]: Parity for res.format and Ctx.Format #2745

nickajacks1 opened this issue Nov 30, 2023 · 4 comments · Fixed by #2766

Comments

@nickajacks1
Copy link
Member

Feature Proposal Description

The current API for Ctx.Format does not align with res.format from Express. While they both perform content negotiation, the usage is different (see below for a comparison). This may be confusing for Gophers coming from Express who expect to be able to add their own handlers.

I propose reworking Ctx.Format to take a mapping of accept types to handlers. We can move the existing Ctx.Format functionality to a separate method (e.g., Ctx.AutoFormat)

Alignment with Express API

In Express, res.format is used to perform content negotiation and call a different handler based on the result. The handlers are fully user-defined aside from the default handler which will send a 406 HTML response.
From https://expressjs.com/en/api.html#res.format

res.format({
  'text/plain': function () {
    res.send('hey')
  },

  'text/html': function () {
    res.send('<p>hey</p>')
  },

  'application/json': function () {
    res.send({ message: 'hey' })
  },

  default: function () {
    // log the request and respond with 406
    res.status(406).send('Not Acceptable')
  }
})

Fiber's Ctx.Format has automatic handlers for a fixed set of Accept headers. The usage is different; it hides details of the request's Accept header and the outgoing Content Type from the implementer.

data := SomeData{ FirstName: "Jane", LastName: "Doe" }
c.Format(data)

See below for proposed syntax.

HTTP RFC Standards Compliance

RFC-9110 does not specify how servers should reply to requests with respect to content - it simply gives various suggestions and algorithms. res.format follows the proactive negotiation algorithm.

API Stability

The new Ctx.Format should be in line with res.format, meaning it should have a stable API. The main risk for API stability is the data structure in which the handlers are passed, as that may have performance considerations (allocations, etc).

Feature Examples

package main

import (
	"fmt"
	"time"

	"github.com/gofiber/fiber/v2"
	"github.com/gofiber/fiber/v2/middleware/logger"
)

type IceCream struct {
	Flavor string `json:"flavor" xml:"flavor"`
	Color  string `json:"color" xml:"color"`
}

func (ic IceCream) String() string {
	return fmt.Sprintf("Ice Cream!\nFlavor: %v\nColor:%v", ic.Flavor, ic.Color)
}

func main() {
	app := fiber.New()
	app.Use("/", logger.New())

	dessert := IceCream{"Strawberry", "Pink"}

	app.Get("/dessert", func(c *fiber.Ctx) error {
		// Since maps are unordered, */* will give us problems, even if
		// we initialize the map once at startup.
		return c.Format(map[string]fiber.Handler{
			"application/json": func(c *fiber.Ctx) error { return c.JSON(dessert) },
			"application/xml":  func(c *fiber.Ctx) error { return c.XML(dessert) },
			"text/plain":       func(c *fiber.Ctx) error { return c.SendString(dessert.String()) },
			"application/vnd.parlor+json": func(c *fiber.Ctx) error {
				return c.JSON(fiber.Map{
					"orderDate": time.Now(),
					"course":    "dessert",
					"food":      "iceCream",
				})
			},
		})
	})

	app.Get("/dessert2", func(c *fiber.Ctx) error {
		// This is a bit verbose. It might need some beautifying.
		return c.Format2(
			fiber.AcceptHandler{
				MediaType: "application/json",
				Handler:   func(c *fiber.Ctx) error { return c.JSON(dessert) },
			},
			fiber.AcceptHandler{
				MediaType: "application/xml",
				Handler:   func(c *fiber.Ctx) error { return c.XML(dessert) },
			},
			fiber.AcceptHandler{
				MediaType: "text/plain",
				Handler:   func(c *fiber.Ctx) error { return c.SendString(dessert.String()) },
			},
			fiber.AcceptHandler{
				MediaType: "application/vnd.parlor+json",
				Handler: func(c *fiber.Ctx) error {
					return c.JSON(fiber.Map{
						"orderDate": time.Now(),
						"course":    "dessert",
						"food":      "iceCream",
					})
				},
			},
		)
	})

	app.Get("/dessert3", func(c *fiber.Ctx) error {
		// Previous c.Format functionality.
		return c.AutoFormat(dessert)
	})

	app.Listen(":3000")
}

Checklist:

  • I agree to follow Fiber's Code of Conduct.
  • I have searched for existing issues that describe my proposal before opening this one.
  • I understand that a proposal that does not meet these guidelines may be closed without explanation.
Copy link

welcome bot commented Nov 30, 2023

Thanks for opening your first issue here! 🎉 Be sure to follow the issue template! If you need help or want to chat with us, join us on Discord https://gofiber.io/discord

@efectn
Copy link
Member

efectn commented Dec 1, 2023

I like the idea. The second one seems good to me. I wouldn't use maps since they are unordered and it will make wildcard unusable.

@efectn efectn added this to the v3 milestone Dec 1, 2023
@efectn efectn added this to v3 Dec 1, 2023
@ReneWerner87
Copy link
Member

yes right
good proposal, i like the change and the alignment with the express functionality

would also vote for the 2nd variant

@nickajacks1
Copy link
Member Author

Completed by #2766

@github-project-automation github-project-automation bot moved this from Todo to Done in v3 Jan 4, 2024
@ReneWerner87 ReneWerner87 linked a pull request Jan 4, 2024 that will close this issue
16 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Done
Development

Successfully merging a pull request may close this issue.

3 participants