My application will serve an API that is available via JSON and XML. There is currently not a convenient way of doing this in Gin. As it is now, I end up with a bunch of switch-cases.
What do you guys say about supporting (maybe a basic implementation) content negotiation (conneg)[0]?
The most simple API could look something like this:
r.GET("/ping", func(c *gin.Context) {
c.Negotiate(200, Response{foo: "Bar"})
})
The response format is then inferred from the Accept-header.
We could also implement a mechanism to try and infer what to return based on URL parameters (/api/?format=xml/json) or file endings (api.json/xml).
Is this something you guys want to move forward with? If so, let me know and I'll implement it.
Interesting!
Anyway, /api/?format=xml/json, api.json/xml do not look very standard.
But using the Accept header looks interesting.
Idea, we could add a:
c.Render(code, binding, data)
it should be used like this:
c.Render(200, binding.JSON, data)
and then add a stric-Accept middleware.
I'm going out of town for at least a week (vacation :)), so if anyone want to jump in on this, please do.
Yeah this popped out at me about gin. I guess I could create a middleware encoder that has the negotiation after the .Next, but it seems like this should be something that is done automatically by gin.
I'm interested in this feature as well. It would be nice to have the control to manipulate the response for each format as well.
:+1: to @alexandernyquist's request for content negotiation
I have a proposal for Content Negociation in Gin:
func (c *Context) NegotiatedFormat() string {
if c.negotiatedFormat != "" {
// Evaluate Accept header
c.negotiatedFormat = "application/json" or "application/xml" or "text/html" ...
}
return c.negotiatedFormat
}
This method is lazily initialized, so the performance will not be affected in the current implementation.
It represents the default content negotiation policy but it can be changed with a middleware by calling:
func (c *Context) SetNegotiatedFormat(format string) {
c.negotiatedFormat = format
}
An API example:
func main() {
r := gin.Default()
r.GET("/hola", func(c *gin.Context) {
data := gin.H{"status": "ok"}
switch c.NegotiateFormat(gin.MIMEHTML, gin.MIMEJSON) {
case gin.MIMEHTML:
c.HTML(200, "resources/hola.tmpl", data)
case gin.MIMEJSON:
c.JSON(200, data)
}
})
}
by default, gin parses the Accept header.
If you want to change the behaviour, just add a middleware:
r.Use(func(c *gin.Context) {
var format struct {
Format `form:"format"`
}
c.Bind(&format)
switch format.Format {
case "xml":
c.SetNegotiatedFormat(gin.MIMEXML)
case "json" || "":
c.SetNegotiatedFormat(gin.MIMEJSON)
default:
c.Fail(406, "Not Acceptable")
}
})
resource?format=json
How about accepting a file extention in the url like /api/resource.json and /api/resource.xml?
How about accepting a file extention in the url like /api/resource.json and /api/resource.xml?
Two ideas:
func main() {
r := gin.Default()
r.Use(func(c *gin.Context) {
extension := c.Params.ByName("ext")
switch extension {
case "json":
c.SetNegotiatedFormat(gin.MIMEJSON)
case "xml":
c.SetNegotiatedFormat(gin.MIMEJSON)
default:
c.Fail(400, "unknown extension")
}
})
r.GET("/resource.:ext", func(c *gin.Context) {
data := gin.H{"status": "ok"}
switch c.NegotiateFormat(gin.MIMEJSON, gin.MIMEXML) {
case gin.MIMEXML:
c.XML(200, data)
case gin.MIMEJSON:
c.JSON(200, data)
}
})
}
package main
import "fmt"
import "github.com/gin-gonic/gin"
import "path/filepath"
func main() {
r := gin.Default()
// Create a route group, so this middleware is just applied to this group
negotiation := r.Group("/", func(c *gin.Context) {
switch filepath.Ext(c.Request.URL.Path); {
case "json" || "":
c.SetNegotiatedFormat(gin.MIMEJSON)
case "xml":
c.SetNegotiatedFormat(gin.MIMEJSON)
default:
c.Fail(400, "unknown extension")
}
})
negotiation.GET("/hola.json", resourceHandler)
negotiation.GET("/hola.xml", resourceHandler)
r.Run(":8080")
}
func resourceHandler(c *gin.Context) {
switch c.NegotiateFormat(gin.MIMEJSON, gin.MIMEXML) {
case gin.MIMEXML:
c.XML(200, data)
case gin.MIMEJSON:
c.JSON(200, gin.H{"status": "ok"})
}
}
I am also testing a new API:
c.Negotiate(200, gin.H{
"html.file": "resouces/resource.tmpl",
"xml.data": xmlData,
"*.data": jsonData,
})
Content.Negotiate()
This is extremely flexible, since you can:
engine.HTMLRender = renderAn update:
c.Negotiate(200, gin.Negotiate{
Offered: []string{gin.MIMEJSON, gin.MIMEXML},
Data: jsonData,
XMLData: xmlData,
})
I think it could be useful to allow extensions to the Negotiate method, because for example the default being an error could not be the best option for everyone, but as it is it is not possible to modify it, without modifying the library code.
Is there a way to register a renderer for a custom MediaType?
I want to use something like application/vnd.myapp.person.json;version=1.0.0 as the "preferred" json format and application/json as a fallback.