add content negotiation feature, add context.ReadYAML and fix https://github.com/kataras/neffos/issues/1#issuecomment-515698536

Former-commit-id: 9753e3e45c7c24788b97814d3ecfb4b03f5ff414
This commit is contained in:
Gerasimos (Makis) Maropoulos 2019-08-03 04:41:09 +03:00
parent 970279deb8
commit b37b369b51
15 changed files with 985 additions and 133 deletions

View File

@ -224,6 +224,7 @@ You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [her
- [Read JSON](http_request/read-json/main.go)
* [Struct Validation](http_request/read-json-struct-validation/main.go)
- [Read XML](http_request/read-xml/main.go)
- [Read YAML](http_request/read-yaml/main.go) **NEW**
- [Read Form](http_request/read-form/main.go)
- [Read Query](http_request/read-query/main.go) **NEW**
- [Read Custom per type](http_request/read-custom-per-type/main.go)
@ -237,6 +238,7 @@ You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [her
### How to Write to `context.ResponseWriter() http.ResponseWriter`
- [Content Negotiation](http_responsewriter/content-negotiation) **NEW**
- [Write `valyala/quicktemplate` templates](http_responsewriter/quicktemplate)
- [Write `shiyanhui/hero` templates](http_responsewriter/herotemplate)
- [Text, Markdown, HTML, JSON, JSONP, XML, Binary](http_responsewriter/write-rest/main.go)

View File

@ -330,6 +330,7 @@ You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [her
- [读取JSON](http_request/read-json/main.go)
- [读取XML](http_request/read-xml/main.go)
- [读取YAML](http_request/read-yaml/main.go) **更新**
- [读取Form](http_request/read-form/main.go)
- [读取Query](http_request/read-query/main.go) **更新**
- [读取每个类型的自定义结果Custom per type](http_request/read-custom-per-type/main.go)
@ -342,6 +343,7 @@ You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [her
### 如何写入`context.ResponseWriter() http.ResponseWriter`
- [Content Negotiation](http_responsewriter/content-negotiation) **更新**
- [`valyala/quicktemplate`模版](http_responsewriter/quicktemplate)
- [`shiyanhui/hero`模版](http_responsewriter/herotemplate)
- [Text, Markdown, HTML, JSON, JSONP, XML, Binary](http_responsewriter/write-rest/main.go)

View File

@ -0,0 +1,36 @@
package main
import (
"github.com/kataras/iris"
)
func newApp() *iris.Application {
app := iris.New()
app.Post("/", handler)
return app
}
// simple yaml stuff, read more at https://yaml.org/start.html
type product struct {
Invoice int `yaml:"invoice"`
Tax float32 `yaml:"tax"`
Total float32 `yaml:"total"`
Comments string `yaml:"comments"`
}
func handler(ctx iris.Context) {
var p product
if err := ctx.ReadYAML(&p); err != nil {
ctx.StatusCode(iris.StatusBadRequest)
ctx.WriteString(err.Error())
return
}
ctx.Writef("Received: %#+v", p)
}
func main() {
app := newApp()
app.Run(iris.Addr(":8080"))
}

View File

@ -0,0 +1,24 @@
package main
import (
"testing"
"github.com/kataras/iris/httptest"
)
func TestReadYAML(t *testing.T) {
app := newApp()
e := httptest.New(t, app)
expectedResponse := `Received: main.product{Invoice:34843, Tax:251.42, Total:4443.52, Comments:"Late afternoon is best. Backup contact is Nancy Billsmer @ 338-4338."}`
send := `invoice: 34843
tax : 251.42
total: 4443.52
comments: >
Late afternoon is best.
Backup contact is Nancy
Billsmer @ 338-4338.`
e.POST("/").WithHeader("Content-Type", "application/x-yaml").WithBytes([]byte(send)).Expect().
Status(httptest.StatusOK).Body().Equal(expectedResponse)
}

View File

@ -0,0 +1,114 @@
// Package main contains three different ways to render content based on the client's accepted.
package main
import "github.com/kataras/iris"
type testdata struct {
Name string `json:"name" xml:"Name"`
Age int `json:"age" xml:"Age"`
}
func newApp() *iris.Application {
app := iris.New()
app.Logger().SetLevel("debug")
// app.Use(func(ctx iris.Context) {
// requestedMime := ctx.URLParamDefault("type", "application/json")
//
// ctx.Negotiation().Accept.Override().MIME(requestedMime, nil)
// ctx.Next()
// })
app.Get("/resource", func(ctx iris.Context) {
data := testdata{
Name: "test name",
Age: 26,
}
// Server allows response only JSON and XML. These values
// are compared with the clients mime needs. Iris comes with default mime types responses
// but you can add a custom one by the `Negotiation().Mime(mime, content)` method,
// same for the "accept".
// You can also pass a custom ContentSelector(mime string) or ContentNegotiator to the
// `Context.Negotiate` method if you want to perform more advanced things.
//
//
// By-default the client accept mime is retrieved by the "Accept" header
// Indeed you can override or update it by `Negotiation().Accept.XXX` i.e
// ctx.Negotiation().Accept.Override().XML()
//
// All these values can change inside middlewares, the `Negotiation().Override()` and `.Accept.Override()`
// can override any previously set values.
// Order matters, if the client accepts anything (*/*)
// then the first prioritized mime's response data will be rendered.
ctx.Negotiation().JSON().XML()
// Accept-Charset vs:
ctx.Negotiation().Charset("utf-8", "iso-8859-7")
// Alternatively you can define the content/data per mime type
// anywhere in the handlers chain using the optional "v" variadic
// input argument of the Context.Negotiation().JSON,XML,YAML,Binary,Text,HTML(...) and e.t.c
// example (order matters):
// ctx.Negotiation().JSON(data).XML(data).Any("content for */*")
// ctx.Negotiate(nil)
// if not nil passed in the `Context.Negotiate` method
// then it overrides any contents made by the negotitation builder above.
_, err := ctx.Negotiate(data)
if err != nil {
ctx.Writef("%v", err)
}
})
app.Get("/resource2", func(ctx iris.Context) {
jsonAndXML := testdata{
Name: "test name",
Age: 26,
}
// I prefer that one, as it gives me the freedom to modify
// response data per accepted mime content type on middlewares as well.
ctx.Negotiation().
JSON(jsonAndXML).
XML(jsonAndXML).
HTML("<h1>Test Name</h1><h2>Age 26</h2>")
ctx.Negotiate(nil)
})
app.Get("/resource3", func(ctx iris.Context) {
// If that line is missing and the requested
// mime type of content is */* or application/xml or application/json
// then 406 Not Acceptable http error code will be rendered instead.
//
// We also add the "gzip" algorithm as an option to encode
// resources on send.
ctx.Negotiation().JSON().XML().HTML().EncodingGzip()
jsonAndXML := testdata{
Name: "test name",
Age: 26,
}
// Prefer that way instead of the '/resource2' above
// if "iris.N" is a static one and can be declared
// outside of a handler.
ctx.Negotiate(iris.N{
// Text: for text/plain,
// Markdown: for text/mardown,
// Binary: for application/octet-stream,
// YAML: for application/x-yaml,
// JSONP: for application/javascript
// Other: for anything else,
JSON: jsonAndXML, // for application/json
XML: jsonAndXML, // for application/xml or text/xml
HTML: "<h1>Test Name</h1><h2>Age 26</h2>", // for text/html
})
})
return app
}
func main() {
app := newApp()
app.Run(iris.Addr(":8080"))
}

View File

@ -0,0 +1,78 @@
package main
import (
"bytes"
"compress/gzip"
"encoding/xml"
"io/ioutil"
"testing"
"github.com/kataras/iris/httptest"
)
func TestContentNegotiation(t *testing.T) {
var (
expectedJSONResponse = testdata{
Name: "test name",
Age: 26,
}
expectedXMLResponse, _ = xml.Marshal(expectedJSONResponse)
expectedHTMLResponse = "<h1>Test Name</h1><h2>Age 26</h2>"
)
e := httptest.New(t, newApp())
e.GET("/resource").WithHeader("Accept", "application/json").
Expect().Status(httptest.StatusOK).
ContentType("application/json", "utf-8").
JSON().Equal(expectedJSONResponse)
e.GET("/resource").WithHeader("Accept", "application/xml").WithHeader("Accept-Charset", "iso-8859-7").
Expect().Status(httptest.StatusOK).
ContentType("application/xml", "iso-8859-7").
Body().Equal(string(expectedXMLResponse))
e.GET("/resource2").WithHeader("Accept", "application/json").
Expect().Status(httptest.StatusOK).
ContentType("application/json", "utf-8").
JSON().Equal(expectedJSONResponse)
e.GET("/resource2").WithHeader("Accept", "application/xml").
Expect().Status(httptest.StatusOK).
ContentType("application/xml", "utf-8").
Body().Equal(string(expectedXMLResponse))
e.GET("/resource2").WithHeader("Accept", "text/html").
Expect().Status(httptest.StatusOK).
ContentType("text/html", "utf-8").
Body().Equal(expectedHTMLResponse)
e.GET("/resource3").WithHeader("Accept", "application/json").
Expect().Status(httptest.StatusOK).
ContentType("application/json", "utf-8").
JSON().Equal(expectedJSONResponse)
e.GET("/resource3").WithHeader("Accept", "application/xml").
Expect().Status(httptest.StatusOK).
ContentType("application/xml", "utf-8").
Body().Equal(string(expectedXMLResponse))
// test html with "gzip" encoding algorithm.
rawGzipResponse := e.GET("/resource3").WithHeader("Accept", "text/html").
WithHeader("Accept-Encoding", "gzip").
Expect().Status(httptest.StatusOK).
ContentType("text/html", "utf-8").
ContentEncoding("gzip").
Body().Raw()
zr, err := gzip.NewReader(bytes.NewReader([]byte(rawGzipResponse)))
if err != nil {
t.Fatal(err)
}
rawResponse, err := ioutil.ReadAll(zr)
if err != nil {
t.Fatal(err)
}
if expected, got := expectedHTMLResponse, string(rawResponse); expected != got {
t.Fatalf("expected response to be:\n%s but got:\n%s", expected, got)
}
}

View File

@ -53,6 +53,8 @@
};
}
const username = window.prompt("Your username?");
async function runExample() {
// You can omit the "default" and simply define only Events, the namespace will be an empty string"",
// however if you decide to make any changes on this example make sure the changes are reflecting inside the ../server.go file as well.
@ -70,6 +72,10 @@
addMessage(msg.Body);
}
}
},{
headers: {
"X-Username": username,
}
});
// You can either wait to conenct or just conn.connect("connect")

View File

@ -42,7 +42,10 @@ func main() {
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(dialAndConnectTimeout))
defer cancel()
client, err := websocket.Dial(ctx, websocket.DefaultGorillaDialer, endpoint, clientEvents)
// username := "my_username"
// dialer := websocket.GobwasDialer(websocket.GobwasDialerOptions{Header: websocket.GobwasHeader{"X-Username": []string{username}}})
dialer := websocket.DefaultGobwasDialer
client, err := websocket.Dial(ctx, dialer, endpoint, clientEvents)
if err != nil {
panic(err)
}

View File

@ -68,8 +68,17 @@ func main() {
SigningMethod: jwt.SigningMethodHS256,
})
idGen := func(ctx iris.Context) string {
if username := ctx.GetHeader("X-Username"); username != "" {
return username
}
return websocket.DefaultIDGenerator(ctx)
}
// serves the endpoint of ws://localhost:8080/echo
websocketRoute := app.Get("/echo", websocket.Handler(websocketServer))
// with optional custom ID generator.
websocketRoute := app.Get("/echo", websocket.Handler(websocketServer, idGen))
if enableJWT {
// Register the jwt middleware (on handshake):

View File

@ -586,6 +586,10 @@ type Context interface {
//
// Example: https://github.com/kataras/iris/blob/master/_examples/http_request/read-xml/main.go
ReadXML(xmlObjectPtr interface{}) error
// ReadYAML reads YAML from request's body and binds it to the "outPtr" value.
//
// Example: https://github.com/kataras/iris/blob/master/_examples/http_request/read-yaml/main.go
ReadYAML(outPtr interface{}) error
// ReadForm binds the formObject with the form data
// it supports any kind of type, including custom structs.
// It will return nothing if request data are empty.
@ -781,6 +785,49 @@ type Context interface {
Markdown(markdownB []byte, options ...Markdown) (int, error)
// YAML parses the "v" using the yaml parser and renders its result to the client.
YAML(v interface{}) (int, error)
// +-----------------------------------------------------------------------+
// | Content Νegotiation |
// | https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation | |
// +-----------------------------------------------------------------------+
// Negotiation creates once and returns the negotiation builder
// to build server-side available content for specific mime type(s)
// and charset(s).
//
// See `Negotiate` method too.
Negotiation() *NegotiationBuilder
// Negotiate used for serving different representations of a resource at the same URI.
//
// The "v" can be a single `N` struct value.
// The "v" can be any value completes the `ContentSelector` interface.
// The "v" can be any value completes the `ContentNegotiator` interface.
// The "v" can be any value of struct(JSON, JSONP, XML, YAML) or
// string(TEXT, HTML) or []byte(Markdown, Binary) or []byte with any matched mime type.
//
// If the "v" is nil, the `Context.Negotitation()` builder's
// content will be used instead, otherwise "v" overrides builder's content
// (server mime types are still retrieved by its registered, supported, mime list)
//
// Set mime type priorities by `Negotiation().JSON().XML().HTML()...`.
// Set charset priorities by `Negotiation().Charset(...)`.
// Set encoding algorithm priorities by `Negotiation().Encoding(...)`.
// Amend the accepted by
// `Negotiation().Accept./Override()/.XML().JSON().Charset(...).Encoding(...)...`.
//
// It returns `ErrContentNotSupported` when not matched mime type(s).
//
// Resources:
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Charset
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding
//
// Supports the above without quality values.
//
// Example at: https://github.com/kataras/iris/blob/master/_examples/http_responsewriter/content-negotiation
Negotiate(v interface{}) (int, error)
// +------------------------------------------------------------+
// | Serve files |
// +------------------------------------------------------------+
@ -1750,7 +1797,9 @@ func (ctx *context) contentTypeOnce(cType string, charset string) {
charset = ctx.Application().ConfigurationReadOnly().GetCharset()
}
cType += "; charset=" + charset
if cType != ContentBinaryHeaderValue {
cType += "; charset=" + charset
}
ctx.Values().Set(contentTypeContextKey, cType)
ctx.writer.Header().Set(ContentTypeHeaderKey, cType)
@ -2396,19 +2445,26 @@ func (ctx *context) shouldOptimize() bool {
// ReadJSON reads JSON from request's body and binds it to a value of any json-valid type.
//
// Example: https://github.com/kataras/iris/blob/master/_examples/http_request/read-json/main.go
func (ctx *context) ReadJSON(jsonObject interface{}) error {
func (ctx *context) ReadJSON(outPtr interface{}) error {
var unmarshaler = json.Unmarshal
if ctx.shouldOptimize() {
unmarshaler = jsoniter.Unmarshal
}
return ctx.UnmarshalBody(jsonObject, UnmarshalerFunc(unmarshaler))
return ctx.UnmarshalBody(outPtr, UnmarshalerFunc(unmarshaler))
}
// ReadXML reads XML from request's body and binds it to a value of any xml-valid type.
//
// Example: https://github.com/kataras/iris/blob/master/_examples/http_request/read-xml/main.go
func (ctx *context) ReadXML(xmlObject interface{}) error {
return ctx.UnmarshalBody(xmlObject, UnmarshalerFunc(xml.Unmarshal))
func (ctx *context) ReadXML(outPtr interface{}) error {
return ctx.UnmarshalBody(outPtr, UnmarshalerFunc(xml.Unmarshal))
}
// ReadYAML reads YAML from request's body and binds it to the "outPtr" value.
//
// Example: https://github.com/kataras/iris/blob/master/_examples/http_request/read-yaml/main.go
func (ctx *context) ReadYAML(outPtr interface{}) error {
return ctx.UnmarshalBody(outPtr, UnmarshalerFunc(yaml.Unmarshal))
}
// IsErrPath can be used at `context#ReadForm` and `context#ReadQuery`.
@ -2893,126 +2949,6 @@ const (
ContentFormMultipartHeaderValue = "multipart/form-data"
)
// TODO:
// const negotitationContextKey = "_iris_accept_negotitation_builder"
// func (ctx *context) Accept() *Negotitation {
// if n := ctx.Values().Get(negotitationContextKey); n != nil {
// return n.(*Negotitation)
// }
// n := new(Negotitation)
// n.accept = parseHeader(ctx.GetHeader("Accept"))
// n.charset = parseHeader(ctx.GetHeader("Accept-Charset"))
// ctx.Values().Set(negotitationContextKey, n)
// return n
// }
// func parseHeader(headerValue string) []string {
// in := strings.Split(headerValue, ",")
// out := make([]string, 0, len(in))
// for _, value := range in {
// // remove any spaces and quality values such as ;q=0.8.
// // */* or * means accept everything.
// v := strings.TrimSpace(strings.Split(value, ";")[0])
// if v != "" {
// out = append(out, v)
// }
// }
// return out
// }
// // Negotitation builds the accepted mime types and charset
// //
// // and "Accept-Charset" headers respectfully.
// // The default values are set by the client side, server can append or override those.
// // The end result will be challenged with runtime preffered set of content types and charsets.
// //
// // See `Negotitate`.
// type Negotitation struct {
// // initialized with "Accept" header values.
// accept []string
// // initialized with "Accept-Charset" and if was empty then the
// // application's default (which defaults to utf-8).
// charset []string
// // To support override in request life cycle.
// // We need slice when data is the same format
// // for one or more mime types,
// // i.e text/xml and obselete application/xml.
// lastAccept []string
// lastCharset []string
// }
// func (n *Negotitation) Override() *Negotitation {
// // when called first.
// n.accept = n.accept[0:0]
// n.charset = n.charset[0:0]
// // when called after.
// if len(n.lastAccept) > 0 {
// n.accept = append(n.accept, n.lastAccept...)
// n.lastAccept = n.lastAccept[0:0]
// }
// if len(n.lastCharset) > 0 {
// n.charset = append(n.charset, n.lastCharset...)
// n.lastCharset = n.lastCharset[0:0]
// }
// return n
// }
// func (n *Negotitation) MIME(mimeType ...string) *Negotitation {
// n.lastAccept = mimeType
// n.accept = append(n.accept, mimeType...)
// return n
// }
// func (n *Negotitation) JSON() *Negotitation {
// return n.MIME(ContentJSONHeaderValue)
// }
// func (n *Negotitation) XML() *Negotitation {
// return n.MIME(ContentXMLHeaderValue, ContentXMLUnreadableHeaderValue)
// }
// func (n *Negotitation) HTML() *Negotitation {
// return n.MIME(ContentHTMLHeaderValue)
// }
// func (n *Negotitation) Charset(charset ...string) *Negotitation {
// n.lastCharset = charset
// n.charset = append(n.charset, charset...)
// return n
// }
// func (n *Negotitation) build(preferences []string) (contentType, charset string) {
// return
// }
// // https://www.w3.org/Protocols/rfc2616/rfc2616-sec12.html
// // https://developer.mozilla.org/en-US/docs/tag/Content%20Negotiation
// // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept
// // https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation/List_of_default_Accept_values
// func (ctx *context) Negotiate(v interface{}, preferences ...string) (int, error) {
// contentType, charset := ctx.Accept().build(preferences)
// // // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Charset
// // If the server cannot serve any matching character set,
// // it can theoretically send back a 406 (Not Acceptable) error code.
// ctx.contentTypeOnce(contentType, charset)
// switch contentType {
// }
// return -1, nil
// }
// Binary writes out the raw bytes as binary data.
func (ctx *context) Binary(data []byte) (int, error) {
ctx.ContentType(ContentBinaryHeaderValue)
@ -3319,6 +3255,627 @@ func (ctx *context) YAML(v interface{}) (int, error) {
return ctx.Write(out)
}
// +-----------------------------------------------------------------------+
// | Content Νegotiation |
// | https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation | |
// +-----------------------------------------------------------------------+
// ErrContentNotSupported returns from the `Negotiate` method
// when server responds with 406.
var ErrContentNotSupported = errors.New("unsupported content")
// ContentSelector is the interface which structs can implement
// to manually choose a content based on the negotiated mime (content type).
// It can be passed to the `Context.Negotiate` method.
//
// See the `N` struct too.
type ContentSelector interface {
SelectContent(mime string) interface{}
}
// ContentNegotiator is the interface which structs can implement
// to override the `Context.Negotiate` default implementation and
// manually respond to the client based on a manuall call of `Context.Negotiation().Build()`
// to get the final negotiated mime and charset.
// It can be passed to the `Context.Negotiate` method.
type ContentNegotiator interface {
// mime and charset can be retrieved by:
// mime, charset := Context.Negotiation().Build()
// Pass this method to `Context.Negotiate` method
// to write custom content.
// Overriding the existing behavior of Context.Negotiate for selecting values based on
// content types, although it can accept any custom mime type with []byte.
// Content type is already set.
// Use it with caution, 99.9% you don't need this but it's here for extreme cases.
Negotiate(ctx Context) (int, error)
}
// N is a struct which can be passed on the `Context.Negotiate` method.
// It contains fields which should be filled based on the `Context.Negotiation()`
// server side values. If no matched mime then its "Other" field will be sent,
// which should be a string or []byte.
// It completes the `ContentSelector` interface.
type N struct {
Text, HTML string
Markdown []byte
Binary []byte
JSON interface{}
JSONP interface{}
XML interface{}
YAML interface{}
Other []byte // custom content types.
}
// SelectContent returns a content based on the matched negotiated "mime".
func (n N) SelectContent(mime string) interface{} {
switch mime {
case ContentTextHeaderValue:
return n.Text
case ContentHTMLHeaderValue:
return n.HTML
case ContentMarkdownHeaderValue:
return n.Markdown
case ContentBinaryHeaderValue:
return n.Binary
case ContentJSONHeaderValue:
return n.JSON
case ContentJavascriptHeaderValue:
return n.JSONP
case ContentXMLHeaderValue, ContentXMLUnreadableHeaderValue:
return n.XML
case ContentYAMLHeaderValue:
return n.YAML
default:
return n.Other
}
}
const negotiationContextKey = "_iris_negotiation_builder"
// Negotiation creates once and returns the negotiation builder
// to build server-side available content for specific mime type(s)
// and charset(s).
//
// See `Negotiate` method too.
func (ctx *context) Negotiation() *NegotiationBuilder {
if n := ctx.Values().Get(negotiationContextKey); n != nil {
return n.(*NegotiationBuilder)
}
acceptBuilder := NegotiationAcceptBuilder{}
acceptBuilder.accept = parseHeader(ctx.GetHeader("Accept"))
acceptBuilder.charset = parseHeader(ctx.GetHeader("Accept-Charset"))
n := &NegotiationBuilder{Accept: acceptBuilder}
ctx.Values().Set(negotiationContextKey, n)
return n
}
func parseHeader(headerValue string) []string {
in := strings.Split(headerValue, ",")
out := make([]string, 0, len(in))
for _, value := range in {
// remove any spaces and quality values such as ;q=0.8.
v := strings.TrimSpace(strings.Split(value, ";")[0])
if v != "" {
out = append(out, v)
}
}
return out
}
// Negotiate used for serving different representations of a resource at the same URI.
//
// The "v" can be a single `N` struct value.
// The "v" can be any value completes the `ContentSelector` interface.
// The "v" can be any value completes the `ContentNegotiator` interface.
// The "v" can be any value of struct(JSON, JSONP, XML, YAML) or
// string(TEXT, HTML) or []byte(Markdown, Binary) or []byte with any matched mime type.
//
// If the "v" is nil, the `Context.Negotitation()` builder's
// content will be used instead, otherwise "v" overrides builder's content
// (server mime types are still retrieved by its registered, supported, mime list)
//
// Set mime type priorities by `Negotiation().JSON().XML().HTML()...`.
// Set charset priorities by `Negotiation().Charset(...)`.
// Set encoding algorithm priorities by `Negotiation().Encoding(...)`.
// Amend the accepted by
// `Negotiation().Accept./Override()/.XML().JSON().Charset(...).Encoding(...)...`.
//
// It returns `ErrContentNotSupported` when not matched mime type(s).
//
// Resources:
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Charset
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding
//
// Supports the above without quality values.
//
// Example at: https://github.com/kataras/iris/blob/master/_examples/http_responsewriter/content-negotiation
func (ctx *context) Negotiate(v interface{}) (int, error) {
contentType, charset, encoding, content := ctx.Negotiation().Build()
if v == nil {
v = content
}
if contentType == "" {
// If the server cannot serve any matching set,
// it can send back a 406 (Not Acceptable) error code.
ctx.StatusCode(http.StatusNotAcceptable)
return -1, ErrContentNotSupported
}
if charset == "" {
charset = ctx.Application().ConfigurationReadOnly().GetCharset()
}
if encoding == "gzip" {
ctx.Gzip(true)
}
ctx.contentTypeOnce(contentType, charset)
if n, ok := v.(ContentNegotiator); ok {
return n.Negotiate(ctx)
}
if s, ok := v.(ContentSelector); ok {
v = s.SelectContent(contentType)
}
// switch v := value.(type) {
// case []byte:
// if contentType == ContentMarkdownHeaderValue {
// return ctx.Markdown(v)
// }
// return ctx.Write(v)
// case string:
// return ctx.WriteString(v)
// default:
// make it switch by content-type only, but we lose custom mime types capability that way:
// ^ solved with []byte on default case and
// ^ N.Other and
// ^ ContentSelector and ContentNegotiator interfaces.
switch contentType {
case ContentTextHeaderValue, ContentHTMLHeaderValue:
return ctx.WriteString(v.(string))
case ContentMarkdownHeaderValue:
return ctx.Markdown(v.([]byte))
case ContentJSONHeaderValue:
return ctx.JSON(v)
case ContentJavascriptHeaderValue:
return ctx.JSONP(v)
case ContentXMLHeaderValue, ContentXMLUnreadableHeaderValue:
return ctx.XML(v)
case ContentYAMLHeaderValue:
return ctx.YAML(v)
default:
// maybe "Other" or v is []byte or string but not a built-in framework mime,
// for custom content types,
// panic if not correct usage.
switch vv := v.(type) {
case []byte:
return ctx.Write(vv)
case string:
return ctx.WriteString(vv)
default:
ctx.StatusCode(http.StatusNotAcceptable)
return -1, ErrContentNotSupported
}
}
}
// NegotiationBuilder returns from the `Context.Negotitation`
// and can be used inside chain of handlers to build server-side
// mime type(s), charset(s) and encoding algorithm(s)
// that should match with the client's
// Accept, Accept-Charset and Accept-Encoding headers (by-default).
// To modify the client's accept use its "Accept" field
// which it's the `NegotitationAcceptBuilder`.
//
// See the `Negotiate` method too.
type NegotiationBuilder struct {
Accept NegotiationAcceptBuilder
mime []string // we need order.
contents map[string]interface{} // map to the "mime" and content should be rendered if that mime requested.
charset []string
encoding []string
}
// MIME registers a mime type and optionally the value that should be rendered
// through `Context.Negotiate` when this mime type is accepted by client.
//
// Returns itself for recursive calls.
func (n *NegotiationBuilder) MIME(mime string, content interface{}) *NegotiationBuilder {
mimes := parseHeader(mime) // if contains more than one sep by commas ",".
if content == nil {
n.mime = append(n.mime, mimes...)
return n
}
if n.contents == nil {
n.contents = make(map[string]interface{})
}
for _, m := range mimes {
n.mime = append(n.mime, m)
n.contents[m] = content
}
return n
}
// Text registers the "text/plain" content type and, optionally,
// a value that `Context.Negotiate` will render
// when a client accepts the "text/plain" content type.
//
// Returns itself for recursive calls.
func (n *NegotiationBuilder) Text(v ...string) *NegotiationBuilder {
var content interface{}
if len(v) > 0 {
content = v[0]
}
return n.MIME(ContentTextHeaderValue, content)
}
// HTML registers the "text/html" content type and, optionally,
// a value that `Context.Negotiate` will render
// when a client accepts the "text/html" content type.
//
// Returns itself for recursive calls.
func (n *NegotiationBuilder) HTML(v ...string) *NegotiationBuilder {
var content interface{}
if len(v) > 0 {
content = v[0]
}
return n.MIME(ContentHTMLHeaderValue, content)
}
// Markdown registers the "text/markdown" content type and, optionally,
// a value that `Context.Negotiate` will render
// when a client accepts the "text/markdown" content type.
//
// Returns itself for recursive calls.
func (n *NegotiationBuilder) Markdown(v ...[]byte) *NegotiationBuilder {
var content interface{}
if len(v) > 0 {
content = v
}
return n.MIME(ContentMarkdownHeaderValue, content)
}
// Binary registers the "application/octet-stream" content type and, optionally,
// a value that `Context.Negotiate` will render
// when a client accepts the "application/octet-stream" content type.
//
// Returns itself for recursive calls.
func (n *NegotiationBuilder) Binary(v ...[]byte) *NegotiationBuilder {
var content interface{}
if len(v) > 0 {
content = v[0]
}
return n.MIME(ContentBinaryHeaderValue, content)
}
// JSON registers the "application/json" content type and, optionally,
// a value that `Context.Negotiate` will render
// when a client accepts the "application/json" content type.
//
// Returns itself for recursive calls.
func (n *NegotiationBuilder) JSON(v ...interface{}) *NegotiationBuilder {
var content interface{}
if len(v) > 0 {
content = v[0]
}
return n.MIME(ContentJSONHeaderValue, content)
}
// JSONP registers the "application/javascript" content type and, optionally,
// a value that `Context.Negotiate` will render
// when a client accepts the "application/javascript" content type.
//
// Returns itself for recursive calls.
func (n *NegotiationBuilder) JSONP(v ...interface{}) *NegotiationBuilder {
var content interface{}
if len(v) > 0 {
content = v[0]
}
return n.MIME(ContentJavascriptHeaderValue, content)
}
// XML registers the "text/xml" and "application/xml" content types and, optionally,
// a value that `Context.Negotiate` will render
// when a client accepts one of the "text/xml" or "application/xml" content types.
//
// Returns itself for recursive calls.
func (n *NegotiationBuilder) XML(v ...interface{}) *NegotiationBuilder {
var content interface{}
if len(v) > 0 {
content = v[0]
}
return n.MIME(ContentXMLHeaderValue+","+ContentXMLUnreadableHeaderValue, content)
}
// YAML registers the "application/x-yaml" content type and, optionally,
// a value that `Context.Negotiate` will render
// when a client accepts the "application/x-yaml" content type.
//
// Returns itself for recursive calls.
func (n *NegotiationBuilder) YAML(v ...interface{}) *NegotiationBuilder {
var content interface{}
if len(v) > 0 {
content = v[0]
}
return n.MIME(ContentYAMLHeaderValue, content)
}
// Any registers a wildcard that can match any client's accept content type.
//
// Returns itself for recursive calls.
func (n *NegotiationBuilder) Any(v ...interface{}) *NegotiationBuilder {
var content interface{}
if len(v) > 0 {
content = v[0]
}
return n.MIME("*", content)
}
// Charset overrides the application's config's charset (which defaults to "utf-8")
// that a client should match for
// (through Accept-Charset header or custom through `NegotitationBuilder.Accept.Override().Charset(...)` call).
// Do not set it if you don't know what you're doing.
//
// Returns itself for recursive calls.
func (n *NegotiationBuilder) Charset(charset ...string) *NegotiationBuilder {
n.charset = append(n.charset, charset...)
return n
}
// Encoding registers one or more encoding algorithms by name, i.e gzip, deflate.
// that a client should match for (through Accept-Encoding header).
//
// Only the "gzip" can be handlded automatically as it's the only builtin encoding algorithm
// to serve resources.
//
// Returns itself for recursive calls.
func (n *NegotiationBuilder) Encoding(encoding ...string) *NegotiationBuilder {
n.encoding = append(n.encoding, encoding...)
return n
}
// EncodingGzip registers the "gzip" encoding algorithm
// that a client should match for (through Accept-Encoding header or call of Accept.Encoding(enc)).
//
// It will make resources to served by "gzip" if Accept-Encoding contains the "gzip" as well.
//
// Returns itself for recursive calls.
func (n *NegotiationBuilder) EncodingGzip() *NegotiationBuilder {
return n.Encoding(GzipHeaderValue)
}
// Build calculates the client's and server's mime type(s), charset(s) and encoding
// and returns the final content type, charset and encoding that server should render
// to the client. It does not clear the fields, use the `Clear` method if neeeded.
//
// The returned "content" can be nil if the matched "contentType" does not provide any value,
// in that case the `Context.Negotiate(v)` must be called with a non-nil value.
func (n *NegotiationBuilder) Build() (contentType, charset, encoding string, content interface{}) {
contentType = negotiationMatch(n.Accept.accept, n.mime)
charset = negotiationMatch(n.Accept.charset, n.charset)
encoding = negotiationMatch(n.Accept.encoding, n.encoding)
if n.contents != nil {
if data, ok := n.contents[contentType]; ok {
content = data
}
}
return
}
// Clear clears the prioritized mime type(s), charset(s) and any contents
// relative to those mime type(s).
// The "Accept" field is stay as it is, use its `Override` method
// to clear out the client's accepted mime type(s) and charset(s).
func (n *NegotiationBuilder) Clear() *NegotiationBuilder {
n.mime = n.mime[0:0]
n.contents = nil
n.charset = n.charset[0:0]
return n
}
func negotiationMatch(in []string, priorities []string) string {
// e.g.
// match json:
// in: text/html, application/json
// prioritities: application/json
// not match:
// in: text/html, application/json
// prioritities: text/xml
// match html:
// in: text/html, application/json
// prioritities: */*
// not match:
// in: application/json
// prioritities: text/xml
// match json:
// in: text/html, application/*
// prioritities: application/json
if len(priorities) == 0 {
return ""
}
if len(in) == 0 {
return priorities[0]
}
for _, accepted := range in {
for _, p := range priorities {
// wildcard is */* or text/* and etc.
// so loop through each char.
for i, n := 0, len(accepted); i < n; i++ {
if accepted[i] != p[i] {
break
}
if accepted[i] == '*' || p[i] == '*' {
return p
}
if i == n-1 {
return p
}
}
}
}
return ""
}
// NegotiationAcceptBuilder builds the accepted mime types and charset
//
// and "Accept-Charset" headers respectfully.
// The default values are set by the client side, server can append or override those.
// The end result will be challenged with runtime preffered set of content types and charsets.
//
// See the `Negotiate` method too.
type NegotiationAcceptBuilder struct {
// initialized with "Accept" request header values.
accept []string
// initialized with "Accept-Encoding" request header. and if was empty then the
// application's default (which defaults to utf-8).
charset []string
// initialized with "Accept-Encoding" request header values.
encoding []string
// To support override in request life cycle.
// We need slice when data is the same format
// for one or more mime types,
// i.e text/xml and obselete application/xml.
lastAccept []string
lastCharset []string
lastEncoding []string
}
// Override clears the default values for accept and accept charset.
// Returns itself.
func (n *NegotiationAcceptBuilder) Override() *NegotiationAcceptBuilder {
// when called first.
n.accept = n.accept[0:0]
n.charset = n.charset[0:0]
n.encoding = n.encoding[0:0]
// when called after.
if len(n.lastAccept) > 0 {
n.accept = append(n.accept, n.lastAccept...)
n.lastAccept = n.lastAccept[0:0]
}
if len(n.lastCharset) > 0 {
n.charset = append(n.charset, n.lastCharset...)
n.lastCharset = n.lastCharset[0:0]
}
if len(n.lastEncoding) > 0 {
n.encoding = append(n.encoding, n.lastEncoding...)
n.lastEncoding = n.lastEncoding[0:0]
}
return n
}
// MIME adds accepted client's mime type(s).
// Returns itself.
func (n *NegotiationAcceptBuilder) MIME(mimeType ...string) *NegotiationAcceptBuilder {
n.lastAccept = mimeType
n.accept = append(n.accept, mimeType...)
return n
}
// Text adds the "text/plain" as accepted client content type.
// Returns itself.
func (n *NegotiationAcceptBuilder) Text() *NegotiationAcceptBuilder {
return n.MIME(ContentTextHeaderValue)
}
// HTML adds the "text/html" as accepted client content type.
// Returns itself.
func (n *NegotiationAcceptBuilder) HTML() *NegotiationAcceptBuilder {
return n.MIME(ContentHTMLHeaderValue)
}
// Markdown adds the "text/markdown" as accepted client content type.
// Returns itself.
func (n *NegotiationAcceptBuilder) Markdown() *NegotiationAcceptBuilder {
return n.MIME(ContentMarkdownHeaderValue)
}
// Binary adds the "application/octet-stream" as accepted client content type.
// Returns itself.
func (n *NegotiationAcceptBuilder) Binary() *NegotiationAcceptBuilder {
return n.MIME(ContentBinaryHeaderValue)
}
// JSON adds the "application/json" as accepted client content type.
// Returns itself.
func (n *NegotiationAcceptBuilder) JSON() *NegotiationAcceptBuilder {
return n.MIME(ContentJSONHeaderValue)
}
// JSONP adds the "application/javascript" as accepted client content type.
// Returns itself.
func (n *NegotiationAcceptBuilder) JSONP() *NegotiationAcceptBuilder {
return n.MIME(ContentJavascriptHeaderValue)
}
// XML adds the "text/xml" and "application/xml" as accepted client content types.
// Returns itself.
func (n *NegotiationAcceptBuilder) XML() *NegotiationAcceptBuilder {
return n.MIME(ContentXMLHeaderValue, ContentXMLUnreadableHeaderValue)
}
// YAML adds the "application/x-yaml" as accepted client content type.
// Returns itself.
func (n *NegotiationAcceptBuilder) YAML() *NegotiationAcceptBuilder {
return n.MIME(ContentYAMLHeaderValue)
}
// Charset adds one or more client accepted charsets.
// Returns itself.
func (n *NegotiationAcceptBuilder) Charset(charset ...string) *NegotiationAcceptBuilder {
n.lastCharset = charset
n.charset = append(n.charset, charset...)
return n
}
// Encoding adds one or more client accepted encoding algorithms.
// Returns itself.
func (n *NegotiationAcceptBuilder) Encoding(encoding ...string) *NegotiationAcceptBuilder {
n.lastEncoding = encoding
n.encoding = append(n.encoding, encoding...)
return n
}
// EncodingGzip adds the "gzip" as accepted encoding.
// Returns itself.
func (n *NegotiationAcceptBuilder) EncodingGzip() *NegotiationAcceptBuilder {
return n.Encoding(GzipHeaderValue)
}
// +------------------------------------------------------------+
// | Serve files |
// +------------------------------------------------------------+

2
go.mod
View File

@ -3,6 +3,7 @@ module github.com/kataras/iris
go 1.12
require (
github.com/kataras/neffos v0.0.9
github.com/BurntSushi/toml v0.3.1
github.com/CloudyKit/jet v2.1.3-0.20180809161101-62edd43e4f88+incompatible // indirect
github.com/Joker/jade v1.0.1-0.20190614124447-d475f43051e7 // indirect
@ -20,7 +21,6 @@ require (
github.com/iris-contrib/i18n v0.0.0-20171121225848-987a633949d0
github.com/json-iterator/go v1.1.6
github.com/kataras/golog v0.0.0-20190624001437-99c81de45f40
github.com/kataras/neffos v0.0.8
github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d // indirect
github.com/mediocregopher/radix/v3 v3.3.0
github.com/microcosm-cc/bluemonday v1.0.2

4
go.sum
View File

@ -1,3 +1,5 @@
github.com/kataras/neffos v0.0.9 h1:P9xJ78Q3/BGEfUJe85L1keVm258ZzaiR5tREPZzGzUY=
github.com/kataras/neffos v0.0.9/go.mod h1:ZYmJC07hQPW67eKuzlfY7SO3bC0mw83A3j6im82hfqw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/CloudyKit/jet v2.1.3-0.20180809161101-62edd43e4f88+incompatible/go.mod h1:HPYO+50pSWkPoj9Q/eq0aRGByCL6ScRlUmiEX5Zgm+w=
github.com/Joker/jade v1.0.1-0.20190614124447-d475f43051e7/go.mod h1:6E6s8o2AE4KhCrqr6GRJjdC/gNfTdxkIXvuGZZda2VM=
@ -15,8 +17,6 @@ github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/
github.com/iris-contrib/i18n v0.0.0-20171121225848-987a633949d0/go.mod h1:pMCz62A0xJL6I+umB2YTlFRwWXaDFA0jy+5HzGiJjqI=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/kataras/golog v0.0.0-20190624001437-99c81de45f40/go.mod h1:PcaEvfvhGsqwXZ6S3CgCbmjcp+4UDUh2MIfF2ZEul8M=
github.com/kataras/neffos v0.0.8 h1:mOv06sotaVbwCszRaRVerbDRP1embTR7y5J5dhnjjYs=
github.com/kataras/neffos v0.0.8/go.mod h1:ZYmJC07hQPW67eKuzlfY7SO3bC0mw83A3j6im82hfqw=
github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d/go.mod h1:NV88laa9UiiDuX9AhMbDPkGYSPugBOV6yTZB1l2K9Z0=
github.com/mediocregopher/radix/v3 v3.3.0/go.mod h1:EmfVyvspXz1uZEyPBMyGK+kjWiKQGvsUt6O3Pj+LDCQ=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=

View File

@ -90,4 +90,12 @@ type (
//
// An alias for the `context/Context#CookieOption`.
CookieOption = context.CookieOption
// N is a struct which can be passed on the `Context.Negotiate` method.
// It contains fields which should be filled based on the `Context.Negotiation()`
// server side values. If no matched mime then its "Other" field will be sent,
// which should be a string or []byte.
// It completes the `context/context.ContentSelector` interface.
//
// An alias for the `context/Context#N`.
N = context.N
)

View File

@ -1,6 +1,8 @@
package websocket
import (
"net/http"
"github.com/kataras/iris/context"
"github.com/kataras/neffos"
@ -138,6 +140,14 @@ func SetDefaultUnmarshaler(fn func(data []byte, v interface{}) error) {
// IDGenerator is an iris-specific IDGenerator for new connections.
type IDGenerator func(context.Context) string
func wrapIDGenerator(idGen IDGenerator) func(ctx context.Context) neffos.IDGenerator {
return func(ctx context.Context) neffos.IDGenerator {
return func(w http.ResponseWriter, r *http.Request) string {
return idGen(ctx)
}
}
}
// Handler returns an Iris handler to be served in a route of an Iris application.
// Accepts the neffos websocket server as its first input argument
// and optionally an Iris-specific `IDGenerator` as its second one.
@ -151,19 +161,19 @@ func Handler(s *neffos.Server, IDGenerator ...IDGenerator) context.Handler {
if ctx.IsStopped() {
return
}
Upgrade(ctx, idGen(ctx), s)
Upgrade(ctx, idGen, s)
}
}
// Upgrade upgrades the request and returns a new websocket Conn.
// Use `Handler` for higher-level implementation instead.
func Upgrade(ctx context.Context, customID string, s *neffos.Server) *neffos.Conn {
func Upgrade(ctx context.Context, idGen IDGenerator, s *neffos.Server) *neffos.Conn {
conn, _ := s.Upgrade(ctx.ResponseWriter(), ctx.Request(), func(socket neffos.Socket) neffos.Socket {
return &socketWrapper{
Socket: socket,
ctx: ctx,
}
}, customID)
}, wrapIDGenerator(idGen)(ctx))
return conn
}

View File

@ -17,6 +17,9 @@ type (
GorillaDialerOptions = gorilla.Options
// GobwasDialerOptions is just an alias for the `gorilla/websocket.Dialer` struct type.
GobwasDialerOptions = gobwas.Options
// GobwasHeader is an alias to the adapter that allows the use of `http.Header` as
// handshake headers.
GobwasHeader = gobwas.Header
// Conn describes the main websocket connection's functionality.
// Its `Connection` will return a new `NSConn` instance.