diff --git a/HISTORY.md b/HISTORY.md index b184f008..d0321457 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -445,6 +445,7 @@ 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.ReadJSONProtobuf(ptr, ...options)` binds JSON request body to a proto message - `Context.ReadMsgPack(ptr)` binds request body of a msgpack format to a struct - `Context.ReadBody(ptr)` binds the request body to the "ptr" depending on the request's Method and Content-Type - `Context.Defer(Handler)` works like `Party.Done` but for the request life-cycle instead diff --git a/_examples/README.md b/_examples/README.md index 4611ff99..3d928073 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -131,6 +131,7 @@ * Response Writer * [Content Negotiation](response-writer/content-negotiation) * [Text, Markdown, YAML, HTML, JSON, JSONP, Msgpack, XML and Binary](response-writer/write-rest/main.go) + * [Protocol Buffers](response-writer/protobuf/main.go) * [Write Gzip](response-writer/write-gzip/main.go) * [HTTP/2 Server Push](response-writer/http2push/main.go) * [Stream Writer](response-writer/stream-writer/main.go) diff --git a/_examples/response-writer/protobuf/README.md b/_examples/response-writer/protobuf/README.md new file mode 100644 index 00000000..4c69034c --- /dev/null +++ b/_examples/response-writer/protobuf/README.md @@ -0,0 +1,18 @@ +# Protocol Buffers + +The `Context.Protobuf(proto.Message)` is the method which sends protos to the client. It accepts a [proto.Message](https://godoc.org/google.golang.org/protobuf/proto#Message) value. + +> Note: Iris is using the newest version of the Go protocol buffers implementation. Read more about it at [The Go Blog: A new Go API for Protocol Buffers](https://blog.golang.org/protobuf-apiv2). + + +1. Install the protoc-gen-go tool. + +```sh +$ go get -u google.golang.org/protobuf/cmd/protoc-gen-go@latest +``` + +2. Generate proto + +```sh +$ protoc -I protos/ protos/hello.proto --go_out=. +``` diff --git a/_examples/response-writer/protobuf/main.go b/_examples/response-writer/protobuf/main.go new file mode 100644 index 00000000..8996c813 --- /dev/null +++ b/_examples/response-writer/protobuf/main.go @@ -0,0 +1,60 @@ +package main + +import ( + "app/protos" + + "github.com/kataras/iris/v12" +) + +func main() { + app := iris.New() + + app.Get("/", send) + app.Get("/json", sendAsJSON) + app.Post("/read", read) + app.Post("/read_json", readFromJSON) + + app.Listen(":8080") +} + +func send(ctx iris.Context) { + response := &protos.HelloReply{Message: "Hello, World!"} + ctx.Protobuf(response) +} + +func sendAsJSON(ctx iris.Context) { + response := &protos.HelloReply{Message: "Hello, World!"} + options := iris.JSON{ + Proto: iris.ProtoMarshalOptions{ + AllowPartial: true, + Multiline: true, + Indent: " ", + }, + } + + ctx.JSON(response, options) +} + +func read(ctx iris.Context) { + var request protos.HelloRequest + + err := ctx.ReadProtobuf(&request) + if err != nil { + ctx.StopWithError(iris.StatusBadRequest, err) + return + } + + ctx.Writef("HelloRequest.Name = %s", request.Name) +} + +func readFromJSON(ctx iris.Context) { + var request protos.HelloRequest + + err := ctx.ReadJSONProtobuf(&request) + if err != nil { + ctx.StopWithError(iris.StatusBadRequest, err) + return + } + + ctx.Writef("HelloRequest.Name = %s", request.Name) +} diff --git a/_examples/response-writer/protobuf/protos/hello.pb.go b/_examples/response-writer/protobuf/protos/hello.pb.go new file mode 100644 index 00000000..ee512210 --- /dev/null +++ b/_examples/response-writer/protobuf/protos/hello.pb.go @@ -0,0 +1,209 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.11.1 +// source: hello.proto + +package protos + +import ( + proto "github.com/golang/protobuf/proto" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// This is a compile-time assertion that a sufficiently up-to-date version +// of the legacy proto package is being used. +const _ = proto.ProtoPackageIsVersion4 + +type HelloRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` +} + +func (x *HelloRequest) Reset() { + *x = HelloRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_hello_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HelloRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HelloRequest) ProtoMessage() {} + +func (x *HelloRequest) ProtoReflect() protoreflect.Message { + mi := &file_hello_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HelloRequest.ProtoReflect.Descriptor instead. +func (*HelloRequest) Descriptor() ([]byte, []int) { + return file_hello_proto_rawDescGZIP(), []int{0} +} + +func (x *HelloRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type HelloReply struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` +} + +func (x *HelloReply) Reset() { + *x = HelloReply{} + if protoimpl.UnsafeEnabled { + mi := &file_hello_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HelloReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HelloReply) ProtoMessage() {} + +func (x *HelloReply) ProtoReflect() protoreflect.Message { + mi := &file_hello_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HelloReply.ProtoReflect.Descriptor instead. +func (*HelloReply) Descriptor() ([]byte, []int) { + return file_hello_proto_rawDescGZIP(), []int{1} +} + +func (x *HelloReply) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +var File_hello_proto protoreflect.FileDescriptor + +var file_hello_proto_rawDesc = []byte{ + 0x0a, 0x0b, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x22, 0x22, 0x0a, 0x0c, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x26, 0x0a, 0x0a, 0x48, 0x65, 0x6c, + 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x42, 0x0a, 0x5a, 0x08, 0x2e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_hello_proto_rawDescOnce sync.Once + file_hello_proto_rawDescData = file_hello_proto_rawDesc +) + +func file_hello_proto_rawDescGZIP() []byte { + file_hello_proto_rawDescOnce.Do(func() { + file_hello_proto_rawDescData = protoimpl.X.CompressGZIP(file_hello_proto_rawDescData) + }) + return file_hello_proto_rawDescData +} + +var file_hello_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_hello_proto_goTypes = []interface{}{ + (*HelloRequest)(nil), // 0: protos.HelloRequest + (*HelloReply)(nil), // 1: protos.HelloReply +} +var file_hello_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_hello_proto_init() } +func file_hello_proto_init() { + if File_hello_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_hello_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HelloRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_hello_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HelloReply); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_hello_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_hello_proto_goTypes, + DependencyIndexes: file_hello_proto_depIdxs, + MessageInfos: file_hello_proto_msgTypes, + }.Build() + File_hello_proto = out.File + file_hello_proto_rawDesc = nil + file_hello_proto_goTypes = nil + file_hello_proto_depIdxs = nil +} diff --git a/_examples/response-writer/protobuf/protos/hello.proto b/_examples/response-writer/protobuf/protos/hello.proto new file mode 100644 index 00000000..31281615 --- /dev/null +++ b/_examples/response-writer/protobuf/protos/hello.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +package protos; + +option go_package = "./protos"; + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} \ No newline at end of file diff --git a/_examples/response-writer/write-rest/main.go b/_examples/response-writer/write-rest/main.go index fd83b6aa..5e757827 100644 --- a/_examples/response-writer/write-rest/main.go +++ b/_examples/response-writer/write-rest/main.go @@ -73,7 +73,9 @@ func main() { }) app.Get("/xml", func(ctx iris.Context) { - ctx.XML(ExampleXML{One: "hello", Two: "xml"}) // or iris.Map{"One":"hello"...} + ctx.XML(ExampleXML{One: "hello", Two: "xml"}) + // OR: + // ctx.XML(iris.XMLMap("keys", iris.Map{"key": "value"})) }) app.Get("/markdown", func(ctx iris.Context) { @@ -82,6 +84,8 @@ func main() { app.Get("/yaml", func(ctx iris.Context) { ctx.YAML(ExampleYAML{Name: "Iris", ServerAddr: "localhost:8080"}) + // OR: + // ctx.YAML(iris.Map{"name": "Iris", "serverAddr": "localhost:8080"}) }) // app.Get("/protobuf", func(ctx iris.Context) { diff --git a/aliases.go b/aliases.go index c696f8f0..22a1098e 100644 --- a/aliases.go +++ b/aliases.go @@ -66,6 +66,10 @@ type ( // // It is an alias of the `context#JSON` type. JSON = context.JSON + // ProtoMarshalOptions is a type alias for protojson.MarshalOptions. + ProtoMarshalOptions = context.ProtoMarshalOptions + // ProtoUnmarshalOptions is a type alias for protojson.UnmarshalOptions. + ProtoUnmarshalOptions = context.ProtoUnmarshalOptions // XML the optional settings for XML renderer. // // It is an alias of the `context#XML` type. diff --git a/context/context.go b/context/context.go index 435cc239..dddee066 100644 --- a/context/context.go +++ b/context/context.go @@ -38,6 +38,7 @@ import ( "github.com/vmihailenco/msgpack/v5" "golang.org/x/net/publicsuffix" "golang.org/x/time/rate" + "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" "gopkg.in/yaml.v3" ) @@ -681,7 +682,11 @@ type Context interface { // Example: https://github.com/kataras/iris/blob/master/_examples/request-body/read-query/main.go ReadQuery(ptr interface{}) error // ReadProtobuf binds the body to the "ptr" of a proto Message and returns any error. + // Look `ReadJSONProtobuf` too. ReadProtobuf(ptr proto.Message) error + // ReadJSONProtobuf reads a JSON body request into the given "ptr" proto.Message. + // Look `ReadProtobuf` too. + ReadJSONProtobuf(ptr proto.Message, opts ...protojson.UnmarshalOptions) error // ReadMsgPack binds the request body of msgpack format to the "ptr" and returns any error. ReadMsgPack(ptr interface{}) error // ReadBody binds the request body to the "ptr" depending on the HTTP Method and the Request's Content-Type. @@ -883,6 +888,8 @@ type Context interface { // HTML writes out a string as text/html. HTML(format string, args ...interface{}) (int, error) // JSON marshals the given interface object and writes the JSON response. + // If the value is a compatible `proto.Message` one + // then it only uses the options.Proto settings to marshal. JSON(v interface{}, options ...JSON) (int, error) // JSONP marshals the given interface object and writes the JSON response. JSONP(v interface{}, options ...JSONP) (int, error) @@ -3000,6 +3007,7 @@ func (ctx *context) ReadQuery(ptr interface{}) error { } // ReadProtobuf binds the body to the "ptr" of a proto Message and returns any error. +// Look `ReadJSONProtobuf` too. func (ctx *context) ReadProtobuf(ptr proto.Message) error { rawData, err := ctx.GetBody() if err != nil { @@ -3009,6 +3017,27 @@ func (ctx *context) ReadProtobuf(ptr proto.Message) error { return proto.Unmarshal(rawData, ptr) } +// ProtoUnmarshalOptions is a type alias for protojson.UnmarshalOptions. +type ProtoUnmarshalOptions = protojson.UnmarshalOptions + +var defaultProtobufUnmarshalOptions ProtoUnmarshalOptions + +// ReadJSONProtobuf reads a JSON body request into the given "ptr" proto.Message. +// Look `ReadProtobuf` too. +func (ctx *context) ReadJSONProtobuf(ptr proto.Message, opts ...ProtoUnmarshalOptions) error { + rawData, err := ctx.GetBody() + if err != nil { + return err + } + + opt := defaultProtobufUnmarshalOptions + if len(opts) > 0 { + opt = opts[1] + } + + return opt.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() @@ -3615,6 +3644,9 @@ func (ctx *context) HTML(format string, args ...interface{}) (int, error) { return ctx.Writef(format, args...) } +// ProtoMarshalOptions is a type alias for protojson.MarshalOptions. +type ProtoMarshalOptions = protojson.MarshalOptions + // JSON contains the options for the JSON (Context's) Renderer. type JSON struct { // http-specific @@ -3625,6 +3657,8 @@ type JSON struct { Prefix string ASCII bool // if true writes with unicode to ASCII content. Secure bool // if true then it prepends a "while(1);" when Go slice (to JSON Array) value. + // proto.Message specific marshal options. + Proto ProtoMarshalOptions } // JSONP contains the options for the JSONP (Context's) Renderer. @@ -3673,6 +3707,15 @@ func WriteJSON(writer io.Writer, v interface{}, options JSON, optimize bool) (in err error ) + if m, ok := v.(proto.Message); ok { + result, err = options.Proto.Marshal(m) + if err != nil { + return 0, err + } + + return writer.Write(result) + } + if !optimize && options.Indent == "" { options.Indent = " " } @@ -3746,6 +3789,8 @@ func stringToBytes(s string) []byte { var DefaultJSONOptions = JSON{} // JSON marshals the given interface object and writes the JSON response to the client. +// If the value is a compatible `proto.Message` one +// then it only uses the options.Proto settings to marshal. func (ctx *context) JSON(v interface{}, opts ...JSON) (n int, err error) { options := DefaultJSONOptions diff --git a/go.mod b/go.mod index 09dd3c9d..2a8ab01d 100644 --- a/go.mod +++ b/go.mod @@ -23,18 +23,18 @@ require ( github.com/kataras/neffos v0.0.16 github.com/kataras/pio v0.0.8 github.com/kataras/sitemap v0.0.5 - github.com/klauspost/compress v1.10.9 + github.com/klauspost/compress v1.10.10 github.com/mediocregopher/radix/v3 v3.5.1 github.com/microcosm-cc/bluemonday v1.0.3 github.com/ryanuber/columnize v2.1.0+incompatible github.com/schollz/closestmatch v2.1.0+incompatible github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect - github.com/square/go-jose/v3 v3.0.0-20200603004136-8ccb8a19e809 + github.com/square/go-jose/v3 v3.0.0-20200622023058-052237293361 github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1 go.etcd.io/bbolt v1.3.5 - golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 - golang.org/x/net v0.0.0-20200602114024-627f9648deb9 - golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd + golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 + golang.org/x/net v0.0.0-20200625001655-4c5254603344 + golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae golang.org/x/text v0.3.3 golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 google.golang.org/protobuf v1.24.0 diff --git a/sessions/sessiondb/redis/database.go b/sessions/sessiondb/redis/database.go index 1ef70f73..71f7040d 100644 --- a/sessions/sessiondb/redis/database.go +++ b/sessions/sessiondb/redis/database.go @@ -31,6 +31,7 @@ type Config struct { Addr string // Clusters a list of network addresses for clusters. // If not empty "Addr" is ignored. + // Currently only Radix() Driver supports it. Clusters []string // Password string .If no password then no 'AUTH'. Defaults to "". Password string