diff --git a/HISTORY.md b/HISTORY.md index e6f81e33..b2c9d35b 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -150,7 +150,7 @@ Prior to this version the `iris.Context` was the only one dependency that has be | `float, float32, float64`, | | | `bool`, | | | `slice` | [Path Parameter](https://github.com/kataras/iris/wiki/Routing-path-parameter-types) | -| Struct | [Request Body](https://github.com/kataras/iris/tree/master/_examples/http_request) of `JSON`, `XML`, `YAML`, `Form`, `URL Query` | +| Struct | [Request Body](https://github.com/kataras/iris/tree/master/_examples/http_request) of `JSON`, `XML`, `YAML`, `Form`, `URL Query`, `Protobuf`, `MsgPack` | Here is a preview of what the new Hero handlers look like: @@ -176,6 +176,10 @@ Other Improvements: New Context Methods: +- `context.Protobuf(proto.Message)` sends protobuf to the client +- `context.MsgPack(interface{})` sends msgpack format data to the client +- `context.ReadProtobuf(ptr)` binds request body to a proto message +- `context.ReadMsgPack(ptr)` binds request body of a msgpack format to a struct - `context.Defer(Handler)` works like `Party.Done` but for the request life-cycle. - `context.ReflectValue() []reflect.Value` stores and returns the `[]reflect.ValueOf(context)` - `context.Controller() reflect.Value` returns the current MVC Controller value (when fired from inside a controller's method). diff --git a/_examples/README.md b/_examples/README.md index a5163d31..6a83c184 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -248,6 +248,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 MsgPack](http_request/read-msgpack/main.go) **NEW** - [Read YAML](http_request/read-yaml/main.go) - [Read Form](http_request/read-form/main.go) - [Read Query](http_request/read-query/main.go) diff --git a/_examples/configuration/README.md b/_examples/configuration/README.md index 3fae71e5..443f0fc1 100644 --- a/_examples/configuration/README.md +++ b/_examples/configuration/README.md @@ -35,7 +35,7 @@ func main() { DisableBodyConsumptionOnUnmarshal: false, DisableAutoFireStatusCode: false, TimeFormat: "Mon, 02 Jan 2006 15:04:05 GMT", - Charset: "UTF-8", + Charset: "utf-8", })) } ``` @@ -60,10 +60,10 @@ func main() { // Prefix: "With", code editors will help you navigate through all // configuration options without even a glitch to the documentation. - app.Listen(":8080", iris.WithoutStartupLog, iris.WithCharset("UTF-8")) + app.Listen(":8080", iris.WithoutStartupLog, iris.WithCharset("utf-8")) // or before run: - // app.Configure(iris.WithoutStartupLog, iris.WithCharset("UTF-8")) + // app.Configure(iris.WithoutStartupLog, iris.WithCharset("utf-8")) // app.Listen(":8080") } ``` @@ -76,7 +76,7 @@ EnablePathEscape = false FireMethodNotAllowed = true DisableBodyConsumptionOnUnmarshal = false TimeFormat = "Mon, 01 Jan 2006 15:04:05 GMT" -Charset = "UTF-8" +Charset = "utf-8" [Other] MyServerName = "iris" diff --git a/_examples/configuration/from-configuration-structure/main.go b/_examples/configuration/from-configuration-structure/main.go index 6e0d0a3a..93908cb4 100644 --- a/_examples/configuration/from-configuration-structure/main.go +++ b/_examples/configuration/from-configuration-structure/main.go @@ -21,7 +21,7 @@ func main() { DisableBodyConsumptionOnUnmarshal: false, DisableAutoFireStatusCode: false, TimeFormat: "Mon, 02 Jan 2006 15:04:05 GMT", - Charset: "UTF-8", + Charset: "utf-8", })) // or before Run: diff --git a/_examples/configuration/from-toml-file/configs/iris.tml b/_examples/configuration/from-toml-file/configs/iris.tml index e5cef93b..449aed92 100644 --- a/_examples/configuration/from-toml-file/configs/iris.tml +++ b/_examples/configuration/from-toml-file/configs/iris.tml @@ -3,7 +3,7 @@ EnablePathEscape = false FireMethodNotAllowed = true DisableBodyConsumptionOnUnmarshal = false TimeFormat = "Mon, 01 Jan 2006 15:04:05 GMT" -Charset = "UTF-8" +Charset = "utf-8" [Other] MyServerName = "iris" diff --git a/_examples/configuration/functional/main.go b/_examples/configuration/functional/main.go index 5eb82c99..c5a3a473 100644 --- a/_examples/configuration/functional/main.go +++ b/_examples/configuration/functional/main.go @@ -15,9 +15,9 @@ func main() { // Prefix: "With", code editors will help you navigate through all // configuration options without even a glitch to the documentation. - app.Listen(":8080", iris.WithoutStartupLog, iris.WithCharset("UTF-8")) + app.Listen(":8080", iris.WithoutStartupLog, iris.WithCharset("utf-8")) // or before run: - // app.Configure(iris.WithoutStartupLog, iris.WithCharset("UTF-8")) + // app.Configure(iris.WithoutStartupLog, iris.WithCharset("utf-8")) // app.Listen(":8080") } diff --git a/_examples/http_request/read-msgpack/main.go b/_examples/http_request/read-msgpack/main.go new file mode 100644 index 00000000..785a9a68 --- /dev/null +++ b/_examples/http_request/read-msgpack/main.go @@ -0,0 +1,38 @@ +package main + +import "github.com/kataras/iris/v12" + +// User example struct to bind to. +type User struct { + Firstname string `msgpack:"firstname"` + Lastname string `msgpack:"lastname"` + City string `msgpack:"city"` + Age int `msgpack:"age"` +} + +// readMsgPack reads a `User` from MsgPack post body. +func readMsgPack(ctx iris.Context) { + var u User + err := ctx.ReadMsgPack(&u) + if err != nil { + ctx.StatusCode(iris.StatusBadRequest) + ctx.WriteString(err.Error()) + return + } + + ctx.Writef("Received: %#+v\n", u) +} + +func main() { + app := iris.New() + app.Post("/", readMsgPack) + + // POST: http://localhost:8080 + // + // To run the example, use a tool like Postman: + // 1. Body: Binary + // 2. Select File, select the one from "_examples/http_responsewriter/write-rest" example. + // The output should be: + // Received: main.User{Firstname:"John", Lastname:"Doe", City:"Neither FBI knows!!!", Age:25} + app.Listen(":8080") +} diff --git a/_examples/http_responsewriter/write-rest/main.go b/_examples/http_responsewriter/write-rest/main.go index 622acb82..27df7ff9 100644 --- a/_examples/http_responsewriter/write-rest/main.go +++ b/_examples/http_responsewriter/write-rest/main.go @@ -7,12 +7,12 @@ import ( "github.com/kataras/iris/v12/context" ) -// User bind struct +// User example struct for json and msgpack. type User struct { - Firstname string `json:"firstname"` - Lastname string `json:"lastname"` - City string `json:"city"` - Age int `json:"age"` + Firstname string `json:"firstname" msgpack:"firstname"` + Lastname string `json:"lastname" msgpack:"lastname"` + City string `json:"city" msgpack:"city"` + Age int `json:"age" msgpack:"age"` } // ExampleXML just a test struct to view represents xml content-type @@ -22,6 +22,12 @@ type ExampleXML struct { Two string `xml:"two,attr"` } +// ExampleYAML just a test struct to write yaml to the client. +type ExampleYAML struct { + Name string `yaml:"name"` + ServerAddr string `yaml:"ServerAddr"` +} + func main() { app := iris.New() @@ -36,7 +42,7 @@ func main() { // Write app.Get("/encode", func(ctx iris.Context) { - peter := User{ + u := User{ Firstname: "John", Lastname: "Doe", City: "Neither FBI knows!!!", @@ -44,7 +50,7 @@ func main() { } // Manually setting a content type: ctx.ContentType("application/javascript") - ctx.JSON(peter) + ctx.JSON(u) }) // Other content types, @@ -74,6 +80,21 @@ func main() { ctx.Markdown([]byte("# Hello Dynamic Markdown -- iris")) }) + app.Get("/yaml", func(ctx iris.Context) { + ctx.YAML(ExampleYAML{Name: "Iris", ServerAddr: "localhost:8080"}) + }) + + app.Get("/msgpack", func(ctx iris.Context) { + u := User{ + Firstname: "John", + Lastname: "Doe", + City: "Neither FBI knows!!!", + Age: 25, + } + + ctx.MsgPack(u) + }) + // http://localhost:8080/decode // http://localhost:8080/encode // @@ -83,6 +104,7 @@ func main() { // http://localhost:8080/jsonp // http://localhost:8080/xml // http://localhost:8080/markdown + // http://localhost:8080/msgpack // // `iris.WithOptimizations` is an optional configurator, // if passed to the `Run` then it will ensure that the application diff --git a/_examples/overview/main.go b/_examples/overview/main.go index 927911c8..83b222cf 100644 --- a/_examples/overview/main.go +++ b/_examples/overview/main.go @@ -83,7 +83,7 @@ func main() { }) // Listen for incoming HTTP/1.x & HTTP/2 clients on localhost port 8080. - app.Listen(":8080", iris.WithCharset("UTF-8")) + app.Listen(":8080", iris.WithCharset("utf-8")) } func logThisMiddleware(ctx iris.Context) { diff --git a/_examples/view/template_html_0/main.go b/_examples/view/template_html_0/main.go index c7aa1730..ead5ec56 100644 --- a/_examples/view/template_html_0/main.go +++ b/_examples/view/template_html_0/main.go @@ -24,7 +24,7 @@ func main() { app.Get("/", hi) // http://localhost:8080 - app.Listen(":8080", iris.WithCharset("UTF-8")) // defaults to that but you can change it. + app.Listen(":8080", iris.WithCharset("utf-8")) // defaults to that but you can change it. } func hi(ctx iris.Context) { diff --git a/configuration.go b/configuration.go index 33a305f6..f46a4b2c 100644 --- a/configuration.go +++ b/configuration.go @@ -803,7 +803,7 @@ type Configuration struct { // Charset character encoding for various rendering // used for templates and the rest of the responses - // Defaults to "UTF-8". + // Defaults to "utf-8". Charset string `json:"charset,omitempty" yaml:"Charset" toml:"Charset"` // PostMaxMemory sets the maximum post data size @@ -1109,7 +1109,7 @@ func DefaultConfiguration() Configuration { DisableBodyConsumptionOnUnmarshal: false, DisableAutoFireStatusCode: false, TimeFormat: "Mon, 02 Jan 2006 15:04:05 GMT", - Charset: "UTF-8", + Charset: "utf-8", // PostMaxMemory is for post body max memory. // diff --git a/configuration_test.go b/configuration_test.go index 5a1b1e6b..3ceae6eb 100644 --- a/configuration_test.go +++ b/configuration_test.go @@ -148,7 +148,7 @@ FireMethodNotAllowed: true EnableOptimizations: true DisableBodyConsumptionOnUnmarshal: true TimeFormat: "Mon, 02 Jan 2006 15:04:05 GMT" -Charset: "UTF-8" +Charset: "utf-8" RemoteAddrHeaders: X-Real-Ip: true @@ -192,7 +192,7 @@ Other: t.Fatalf("error on TestConfigurationYAML: Expected TimeFormat %s but got %s", expected, c.TimeFormat) } - if expected := "UTF-8"; c.Charset != expected { + if expected := "utf-8"; c.Charset != expected { t.Fatalf("error on TestConfigurationYAML: Expected Charset %s but got %s", expected, c.Charset) } @@ -245,7 +245,7 @@ FireMethodNotAllowed = true EnableOptimizations = true DisableBodyConsumptionOnUnmarshal = true TimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT" -Charset = "UTF-8" +Charset = "utf-8" [RemoteAddrHeaders] X-Real-Ip = true @@ -291,7 +291,7 @@ Charset = "UTF-8" t.Fatalf("error on TestConfigurationTOML: Expected TimeFormat %s but got %s", expected, c.TimeFormat) } - if expected := "UTF-8"; c.Charset != expected { + if expected := "utf-8"; c.Charset != expected { t.Fatalf("error on TestConfigurationTOML: Expected Charset %s but got %s", expected, c.Charset) } diff --git a/context/context.go b/context/context.go index f3291a17..7cbcc974 100644 --- a/context/context.go +++ b/context/context.go @@ -28,10 +28,12 @@ import ( "github.com/Shopify/goreferrer" "github.com/fatih/structs" + "github.com/golang/protobuf/proto" "github.com/iris-contrib/blackfriday" "github.com/iris-contrib/schema" jsoniter "github.com/json-iterator/go" "github.com/microcosm-cc/bluemonday" + "github.com/vmihailenco/msgpack/v5" "gopkg.in/yaml.v3" ) @@ -607,17 +609,21 @@ type Context interface { // // 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. + // ReadForm binds the request body of a form to the "formObject". + // It supports any kind of type, including custom structs. // It will return nothing if request data are empty. // The struct field tag is "form". // // Example: https://github.com/kataras/iris/blob/master/_examples/http_request/read-form/main.go ReadForm(formObject interface{}) error - // ReadQuery binds the "ptr" with the url query string. The struct field tag is "url". + // ReadQuery binds url query to "ptr". The struct field tag is "url". // // Example: https://github.com/kataras/iris/blob/master/_examples/http_request/read-query/main.go ReadQuery(ptr interface{}) error + // ReadProtobuf binds the body to the "ptr" of a proto Message and returns any error. + ReadProtobuf(ptr proto.Message) error + // ReadMsgPack binds the request body of msgpack format to the "ptr" and returns any error. + ReadMsgPack(ptr interface{}) error // +------------------------------------------------------------+ // | Body (raw) Writers | // +------------------------------------------------------------+ @@ -815,6 +821,10 @@ 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) + // Protobuf parses the "v" of proto Message and renders its result to the client. + Protobuf(v proto.Message) (int, error) + // MsgPack parses the "v" of msgpack format and renders its result to the client. + MsgPack(v interface{}) (int, error) // +-----------------------------------------------------------------------+ // | Content Νegotiation | @@ -2650,8 +2660,8 @@ func (ctx *context) ReadYAML(outPtr interface{}) error { // A shortcut for the `schema#IsErrPath`. var IsErrPath = schema.IsErrPath -// ReadForm binds the formObject with the form data -// it supports any kind of type, including custom structs. +// ReadForm binds the request body of a form to the "formObject". +// It supports any kind of type, including custom structs. // It will return nothing if request data are empty. // The struct field tag is "form". // @@ -2665,7 +2675,7 @@ func (ctx *context) ReadForm(formObject interface{}) error { return schema.DecodeForm(values, formObject) } -// ReadQuery binds the "ptr" with the url query string. The struct field tag is "url". +// ReadQuery binds url query to "ptr". The struct field tag is "url". // // Example: https://github.com/kataras/iris/blob/master/_examples/http_request/read-query/main.go func (ctx *context) ReadQuery(ptr interface{}) error { @@ -2677,6 +2687,26 @@ func (ctx *context) ReadQuery(ptr interface{}) error { return schema.DecodeQuery(values, ptr) } +// ReadProtobuf binds the body to the "ptr" of a proto Message and returns any error. +func (ctx *context) ReadProtobuf(ptr proto.Message) error { + rawData, err := ctx.GetBody() + if err != nil { + return err + } + + return proto.Unmarshal(rawData, ptr) +} + +// ReadMsgPack binds the request body of msgpack format to the "ptr" and returns any error. +func (ctx *context) ReadMsgPack(ptr interface{}) error { + rawData, err := ctx.GetBody() + if err != nil { + return err + } + + return msgpack.Unmarshal(rawData, ptr) +} + // +------------------------------------------------------------+ // | Body (raw) Writers | // +------------------------------------------------------------+ @@ -3140,6 +3170,12 @@ const ( ContentMarkdownHeaderValue = "text/markdown" // ContentYAMLHeaderValue header value for YAML data. ContentYAMLHeaderValue = "application/x-yaml" + // ContentProtobufHeaderValue header value for Protobuf messages data. + ContentProtobufHeaderValue = "application/x-protobuf" + // ContentMsgPackHeaderValue header value for MsgPack data. + ContentMsgPackHeaderValue = "application/msgpack" + // ContentMsgPack2HeaderValue alternative header value for MsgPack data. + ContentMsgPack2HeaderValue = "application/x-msgpack" // ContentFormHeaderValue header value for post form data. ContentFormHeaderValue = "application/x-www-form-urlencoded" // ContentFormMultipartHeaderValue header value for post multipart form data. @@ -3583,6 +3619,28 @@ func (ctx *context) YAML(v interface{}) (int, error) { return ctx.Write(out) } +// Protobuf parses the "v" of proto Message and renders its result to the client. +func (ctx *context) Protobuf(v proto.Message) (int, error) { + out, err := proto.Marshal(v) + if err != nil { + return 0, err + } + + ctx.ContentType(ContentProtobufHeaderValue) + return ctx.Write(out) +} + +// MsgPack parses the "v" of msgpack format and renders its result to the client. +func (ctx *context) MsgPack(v interface{}) (int, error) { + out, err := msgpack.Marshal(v) + if err != nil { + return 0, err + } + + ctx.ContentType(ContentMsgPackHeaderValue) + return ctx.Write(out) +} + // +-----------------------------------------------------------------------+ // | Content Νegotiation | // | https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation | | @@ -3628,11 +3686,13 @@ type N struct { Markdown []byte Binary []byte - JSON interface{} - Problem Problem - JSONP interface{} - XML interface{} - YAML interface{} + JSON interface{} + Problem Problem + JSONP interface{} + XML interface{} + YAML interface{} + Protobuf interface{} + MsgPack interface{} Other []byte // custom content types. } @@ -3658,12 +3718,16 @@ func (n N) SelectContent(mime string) interface{} { return n.XML case ContentYAMLHeaderValue: return n.YAML + case ContentProtobufHeaderValue: + return n.Protobuf + case ContentMsgPackHeaderValue, ContentMsgPack2HeaderValue: + return n.MsgPack default: return n.Other } } -const negotiationContextKey = "_iris_negotiation_builder" +const negotiationContextKey = "iris.negotiation_builder" // Negotiation creates once and returns the negotiation builder // to build server-side available prioritized content @@ -3706,7 +3770,7 @@ func parseHeader(headerValue string) []string { // 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 +// The "v" can be any value of struct(JSON, JSONP, XML, YAML, Protobuf, MsgPack) 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 @@ -3791,6 +3855,15 @@ func (ctx *context) Negotiate(v interface{}) (int, error) { return ctx.XML(v) case ContentYAMLHeaderValue: return ctx.YAML(v) + case ContentProtobufHeaderValue: + msg, ok := v.(proto.Message) + if !ok { + return -1, ErrContentNotSupported + } + + return ctx.Protobuf(msg) + case ContentMsgPackHeaderValue, ContentMsgPack2HeaderValue: + return ctx.MsgPack(v) default: // maybe "Other" or v is []byte or string but not a built-in framework mime, // for custom content types, @@ -3966,6 +4039,32 @@ func (n *NegotiationBuilder) YAML(v ...interface{}) *NegotiationBuilder { return n.MIME(ContentYAMLHeaderValue, content) } +// Protobuf registers the "application/x-protobuf" content type and, optionally, +// a value that `Context.Negotiate` will render +// when a client accepts the "application/x-protobuf" content type. +// +// Returns itself for recursive calls. +func (n *NegotiationBuilder) Protobuf(v ...interface{}) *NegotiationBuilder { + var content interface{} + if len(v) > 0 { + content = v[0] + } + return n.MIME(ContentProtobufHeaderValue, content) +} + +// MsgPack registers the "application/x-msgpack" and "application/msgpack" content types and, optionally, +// a value that `Context.Negotiate` will render +// when a client accepts one of the "application/x-msgpack" or "application/msgpack" content types. +// +// Returns itself for recursive calls. +func (n *NegotiationBuilder) MsgPack(v ...interface{}) *NegotiationBuilder { + var content interface{} + if len(v) > 0 { + content = v[0] + } + return n.MIME(ContentMsgPackHeaderValue+","+ContentMsgPack2HeaderValue, content) +} + // Any registers a wildcard that can match any client's accept content type. // // Returns itself for recursive calls. diff --git a/go.mod b/go.mod index 79cd57ab..ca52e7b4 100644 --- a/go.mod +++ b/go.mod @@ -12,13 +12,14 @@ require ( github.com/etcd-io/bbolt v1.3.3 github.com/fatih/structs v1.1.0 github.com/gavv/httpexpect v2.0.0+incompatible + github.com/golang/protobuf v1.3.5 github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38 github.com/hashicorp/go-version v1.2.0 github.com/iris-contrib/blackfriday v2.0.0+incompatible github.com/iris-contrib/go.uuid v2.0.0+incompatible + github.com/iris-contrib/jade v1.1.3 github.com/iris-contrib/pongo2 v0.0.1 github.com/iris-contrib/schema v0.0.1 - github.com/iris-contrib/jade v1.1.3 github.com/json-iterator/go v1.1.9 github.com/kataras/golog v0.0.10 github.com/kataras/neffos v0.0.14 @@ -28,8 +29,8 @@ require ( github.com/microcosm-cc/bluemonday v1.0.2 github.com/ryanuber/columnize v2.1.0+incompatible github.com/schollz/closestmatch v2.1.0+incompatible + github.com/vmihailenco/msgpack/v5 v5.0.0-alpha.2 golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876 - golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 golang.org/x/text v0.3.2 gopkg.in/ini.v1 v1.51.1 gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2 diff --git a/hero/binding.go b/hero/binding.go index c34ebe62..c9ab240b 100644 --- a/hero/binding.go +++ b/hero/binding.go @@ -7,6 +7,8 @@ import ( "strings" "github.com/kataras/iris/v12/context" + + "github.com/golang/protobuf/proto" ) // binding contains the Dependency and the Input, it's the result of a function or struct + dependencies. @@ -344,6 +346,14 @@ func payloadBinding(index int, typ reflect.Type) *binding { err = ctx.ReadForm(ptr) case context.ContentJSONHeaderValue: err = ctx.ReadJSON(ptr) + case context.ContentProtobufHeaderValue: + if msg, ok := ptr.(proto.Message); ok { + err = ctx.ReadProtobuf(msg) + } else { + err = context.ErrContentNotSupported + } + case context.ContentMsgPackHeaderValue, context.ContentMsgPack2HeaderValue: + err = ctx.ReadMsgPack(ptr) default: if ctx.Request().URL.RawQuery != "" { // try read from query. diff --git a/iris.go b/iris.go index 7000fbee..1fbe87e8 100644 --- a/iris.go +++ b/iris.go @@ -965,7 +965,7 @@ func (app *Application) Listen(hostPort string, withOrWithout ...Configurator) e // then create a new host and run it manually by `go NewHost(*http.Server).Serve/ListenAndServe` etc... // or use an already created host: // h := NewHost(*http.Server) -// Run(Raw(h.ListenAndServe), WithCharset("UTF-8"), WithRemoteAddrHeader("CF-Connecting-IP")) +// Run(Raw(h.ListenAndServe), WithCharset("utf-8"), WithRemoteAddrHeader("CF-Connecting-IP")) // // The Application can go online with any type of server or iris's host with the help of // the following runners: