New: gRPC MVC features, new WithLowercaseRouting option and add some new context methods

read HISTORY.md


Former-commit-id: 30a16cceb11f754aa32923058abeda1e736350e7
This commit is contained in:
Gerasimos (Makis) Maropoulos 2020-04-25 02:30:19 +03:00
parent 0cf5d5a4a3
commit 5d3c96947c
21 changed files with 566 additions and 185 deletions

View File

@ -176,6 +176,14 @@ Here is a preview of what the new Hero handlers look like:
Other Improvements:
- [gRPC](https://grpc.io/) features:
- New Router [Wrapper](middleware/grpc).
- New MVC `.Handle(ctrl, mvc.GRPC{...})` option which allows to register gRPC services per-party (without the requirement of a full wrapper) and optionally strict access to gRPC clients only, see the [example here](_examples/mvc/grpc-compatible).
- Improved logging (with `app.Logger().SetLevel("debug")`) for MVC-registered routes.
- New `iris.WithLowercaseRouting` option which forces all routes' paths to be lowercase and converts request paths to their lowercase for matching.
- New `app.Validator { Struct(interface{}) error }` field and `app.Validate` method were added. The `app.Validator = ` can be used to integrate a 3rd-party package such as [go-playground/validator](https://github.com/go-playground/validator). If set-ed then Iris `Context`'s `ReadJSON`, `ReadXML`, `ReadMsgPack`, `ReadYAML`, `ReadForm`, `ReadQuery`, `ReadBody` methods will return the validation error on data validation failures. The [read-json-struct-validation](_examples/http_request/read-json-struct-validation) example was updated.
- A result of <T> can implement the new `hero.PreflightResult` interface which contains a single method of `Preflight(iris.Context) error`. If this method exists on a custom struct value which is returned from a handler then it will fire that `Preflight` first and if not errored then it will cotninue by sending the struct value as JSON(by-default) response body.
@ -184,10 +192,16 @@ Other Improvements:
- Hero Handlers (and `app.ConfigureContainer().Handle`) do not have to require `iris.Context` just to call `ctx.Next()` anymore, this is done automatically now.
- Improve Remote Address parsing as requested at: https://github.com/kataras/iris/issues/1453. Add `Configuration.RemoteAddrPrivateSubnets` to exclude those addresses when fetched by `Configuration.RemoteAddrHeaders` through `context.RemoteAddr() string`.
- Improve Remote Address parsing as requested at: [#1453](https://github.com/kataras/iris/issues/1453). Add `Configuration.RemoteAddrPrivateSubnets` to exclude those addresses when fetched by `Configuration.RemoteAddrHeaders` through `context.RemoteAddr() string`.
- Fix [#1487](https://github.com/kataras/iris/issues/1487).
- Fix [#1473](https://github.com/kataras/iris/issues/1473).
New Context Methods:
- `context.IsHTTP2() bool` reports whether the protocol version for incoming request was HTTP/2
- `context.IsGRPC() bool` reports whether the request came from a gRPC client
- `context.UpsertCookie(*http.Cookie, cookieOptions ...context.CookieOption)` upserts a cookie, fixes [#1485](https://github.com/kataras/iris/issues/1485) too
- `context.StopWithStatus(int)` stops the handlers chain and writes the status code
- `context.StopWithJSON(int, interface{})` stops the handlers chain, writes the status code and sends a JSON response

View File

@ -0,0 +1,20 @@
# gRPC Iris Example
## Generate TLS Keys
```sh
$ openssl genrsa -out server.key 2048
$ openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650
```
## Install the protoc Go plugin
```sh
$ go get -u github.com/golang/protobuf/protoc-gen-go
```
## Generate proto
```sh
$ protoc -I helloworld/ helloworld/helloworld.proto --go_out=plugins=grpc:helloworld
```

View File

@ -39,7 +39,7 @@ func main() {
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := c.PostHello(ctx, &pb.HelloRequest{Name: name})
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
if err != nil {
log.Fatalf("could not greet: %v", err)
}

View File

@ -1,130 +1,240 @@
// Copyright 2015 gRPC authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.21.0
// protoc v3.11.1
// source: helloworld.proto
package helloworld
import (
context "context"
fmt "fmt"
math "math"
proto "github.com/golang/protobuf/proto"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
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 to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
// This is a compile-time assertion that a sufficiently up-to-date version
// of the legacy proto package is being used.
const _ = proto.ProtoPackageIsVersion4
// The request message containing the user's name.
type HelloRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *HelloRequest) Reset() { *m = HelloRequest{} }
func (m *HelloRequest) String() string { return proto.CompactTextString(m) }
func (x *HelloRequest) Reset() {
*x = HelloRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_helloworld_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_helloworld_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 fileDescriptor_17b8c58d586b62f2, []int{0}
return file_helloworld_proto_rawDescGZIP(), []int{0}
}
func (m *HelloRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_HelloRequest.Unmarshal(m, b)
}
func (m *HelloRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_HelloRequest.Marshal(b, m, deterministic)
}
func (m *HelloRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_HelloRequest.Merge(m, src)
}
func (m *HelloRequest) XXX_Size() int {
return xxx_messageInfo_HelloRequest.Size(m)
}
func (m *HelloRequest) XXX_DiscardUnknown() {
xxx_messageInfo_HelloRequest.DiscardUnknown(m)
}
var xxx_messageInfo_HelloRequest proto.InternalMessageInfo
func (m *HelloRequest) GetName() string {
if m != nil {
return m.Name
func (x *HelloRequest) GetName() string {
if x != nil {
return x.Name
}
return ""
}
// The response message containing the greetings
type HelloReply struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *HelloReply) Reset() { *m = HelloReply{} }
func (m *HelloReply) String() string { return proto.CompactTextString(m) }
func (x *HelloReply) Reset() {
*x = HelloReply{}
if protoimpl.UnsafeEnabled {
mi := &file_helloworld_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_helloworld_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 fileDescriptor_17b8c58d586b62f2, []int{1}
return file_helloworld_proto_rawDescGZIP(), []int{1}
}
func (m *HelloReply) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_HelloReply.Unmarshal(m, b)
}
func (m *HelloReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_HelloReply.Marshal(b, m, deterministic)
}
func (m *HelloReply) XXX_Merge(src proto.Message) {
xxx_messageInfo_HelloReply.Merge(m, src)
}
func (m *HelloReply) XXX_Size() int {
return xxx_messageInfo_HelloReply.Size(m)
}
func (m *HelloReply) XXX_DiscardUnknown() {
xxx_messageInfo_HelloReply.DiscardUnknown(m)
}
var xxx_messageInfo_HelloReply proto.InternalMessageInfo
func (m *HelloReply) GetMessage() string {
if m != nil {
return m.Message
func (x *HelloReply) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
func init() {
proto.RegisterType((*HelloRequest)(nil), "helloworld.HelloRequest")
proto.RegisterType((*HelloReply)(nil), "helloworld.HelloReply")
var File_helloworld_proto protoreflect.FileDescriptor
var file_helloworld_proto_rawDesc = []byte{
0x0a, 0x10, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x12, 0x0a, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 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, 0x32, 0x49, 0x0a, 0x07, 0x47, 0x72,
0x65, 0x65, 0x74, 0x65, 0x72, 0x12, 0x3e, 0x0a, 0x08, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c,
0x6f, 0x12, 0x18, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48,
0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x68, 0x65,
0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65,
0x70, 0x6c, 0x79, 0x22, 0x00, 0x42, 0x30, 0x0a, 0x1b, 0x69, 0x6f, 0x2e, 0x67, 0x72, 0x70, 0x63,
0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77,
0x6f, 0x72, 0x6c, 0x64, 0x42, 0x0f, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x57, 0x6f, 0x72, 0x6c, 0x64,
0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
func init() { proto.RegisterFile("helloworld.proto", fileDescriptor_17b8c58d586b62f2) }
var (
file_helloworld_proto_rawDescOnce sync.Once
file_helloworld_proto_rawDescData = file_helloworld_proto_rawDesc
)
var fileDescriptor_17b8c58d586b62f2 = []byte{
// 175 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0xc8, 0x48, 0xcd, 0xc9,
0xc9, 0x2f, 0xcf, 0x2f, 0xca, 0x49, 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x42, 0x88,
0x28, 0x29, 0x71, 0xf1, 0x78, 0x80, 0x78, 0x41, 0xa9, 0x85, 0xa5, 0xa9, 0xc5, 0x25, 0x42, 0x42,
0x5c, 0x2c, 0x79, 0x89, 0xb9, 0xa9, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0x9c, 0x41, 0x60, 0xb6, 0x92,
0x1a, 0x17, 0x17, 0x54, 0x4d, 0x41, 0x4e, 0xa5, 0x90, 0x04, 0x17, 0x7b, 0x6e, 0x6a, 0x71, 0x71,
0x62, 0x3a, 0x4c, 0x11, 0x8c, 0x6b, 0xe4, 0xc9, 0xc5, 0xee, 0x5e, 0x94, 0x9a, 0x5a, 0x92, 0x5a,
0x24, 0x64, 0xc7, 0xc5, 0x11, 0x9c, 0x58, 0x09, 0xd6, 0x25, 0x24, 0xa1, 0x87, 0xe4, 0x02, 0x64,
0xcb, 0xa4, 0xc4, 0xb0, 0xc8, 0x14, 0xe4, 0x54, 0x2a, 0x31, 0x38, 0x19, 0x70, 0x49, 0x67, 0xe6,
0xeb, 0xa5, 0x17, 0x15, 0x24, 0xeb, 0xa5, 0x56, 0x24, 0xe6, 0x16, 0xe4, 0xa4, 0x16, 0x23, 0xa9,
0x75, 0xe2, 0x07, 0x2b, 0x0e, 0x07, 0xb1, 0x03, 0x40, 0x5e, 0x0a, 0x60, 0x4c, 0x62, 0x03, 0xfb,
0xcd, 0x18, 0x10, 0x00, 0x00, 0xff, 0xff, 0x0f, 0xb7, 0xcd, 0xf2, 0xef, 0x00, 0x00, 0x00,
func file_helloworld_proto_rawDescGZIP() []byte {
file_helloworld_proto_rawDescOnce.Do(func() {
file_helloworld_proto_rawDescData = protoimpl.X.CompressGZIP(file_helloworld_proto_rawDescData)
})
return file_helloworld_proto_rawDescData
}
var file_helloworld_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_helloworld_proto_goTypes = []interface{}{
(*HelloRequest)(nil), // 0: helloworld.HelloRequest
(*HelloReply)(nil), // 1: helloworld.HelloReply
}
var file_helloworld_proto_depIdxs = []int32{
0, // 0: helloworld.Greeter.SayHello:input_type -> helloworld.HelloRequest
1, // 1: helloworld.Greeter.SayHello:output_type -> helloworld.HelloReply
1, // [1:2] is the sub-list for method output_type
0, // [0:1] 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_helloworld_proto_init() }
func file_helloworld_proto_init() {
if File_helloworld_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_helloworld_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_helloworld_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_helloworld_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_helloworld_proto_goTypes,
DependencyIndexes: file_helloworld_proto_depIdxs,
MessageInfos: file_helloworld_proto_msgTypes,
}.Build()
File_helloworld_proto = out.File
file_helloworld_proto_rawDesc = nil
file_helloworld_proto_goTypes = nil
file_helloworld_proto_depIdxs = nil
}
// Reference imports to suppress errors if they are not otherwise used.
@ -140,7 +250,7 @@ const _ = grpc.SupportPackageIsVersion6
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type GreeterClient interface {
// Sends a greeting
PostHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)
SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)
}
type greeterClient struct {
@ -151,9 +261,9 @@ func NewGreeterClient(cc grpc.ClientConnInterface) GreeterClient {
return &greeterClient{cc}
}
func (c *greeterClient) PostHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) {
func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) {
out := new(HelloReply)
err := c.cc.Invoke(ctx, "/helloworld.Greeter/PostHello", in, out, opts...)
err := c.cc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, opts...)
if err != nil {
return nil, err
}
@ -163,15 +273,15 @@ func (c *greeterClient) PostHello(ctx context.Context, in *HelloRequest, opts ..
// GreeterServer is the server API for Greeter service.
type GreeterServer interface {
// Sends a greeting
PostHello(context.Context, *HelloRequest) (*HelloReply, error)
SayHello(context.Context, *HelloRequest) (*HelloReply, error)
}
// UnimplementedGreeterServer can be embedded to have forward compatible implementations.
type UnimplementedGreeterServer struct {
}
func (*UnimplementedGreeterServer) PostHello(ctx context.Context, req *HelloRequest) (*HelloReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method PostHello not implemented")
func (*UnimplementedGreeterServer) SayHello(context.Context, *HelloRequest) (*HelloReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented")
}
func RegisterGreeterServer(s *grpc.Server, srv GreeterServer) {
@ -184,14 +294,14 @@ func _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(in
return nil, err
}
if interceptor == nil {
return srv.(GreeterServer).PostHello(ctx, in)
return srv.(GreeterServer).SayHello(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/helloworld.Greeter/PostHello",
FullMethod: "/helloworld.Greeter/SayHello",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GreeterServer).PostHello(ctx, req.(*HelloRequest))
return srv.(GreeterServer).SayHello(ctx, req.(*HelloRequest))
}
return interceptor(ctx, in, info, handler)
}
@ -201,7 +311,7 @@ var _Greeter_serviceDesc = grpc.ServiceDesc{
HandlerType: (*GreeterServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "PostHello",
MethodName: "SayHello",
Handler: _Greeter_SayHello_Handler,
},
},

View File

@ -1,3 +1,17 @@
// Copyright 2015 gRPC authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
option java_multiple_files = true;
@ -9,7 +23,7 @@ package helloworld;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc PostHello (HelloRequest) returns (HelloReply) {}
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.

View File

@ -6,7 +6,6 @@ import (
pb "github.com/kataras/iris/v12/_examples/mvc/grpc-compatible/helloworld"
"github.com/kataras/iris/v12"
grpcWrapper "github.com/kataras/iris/v12/middleware/grpc"
"github.com/kataras/iris/v12/mvc"
"google.golang.org/grpc"
@ -23,7 +22,8 @@ func main() {
app := newApp()
app.Logger().SetLevel("debug")
// POST: https://localhost/hello
// The Iris server should ran under TLS (it's a gRPC requirement).
// POST: https://localhost:443/helloworld.greeter/sayhello
// with request data: {"name": "John"}
// and expected output: {"message": "Hello John"}
app.Run(iris.TLS(":443", "server.crt", "server.key"))
@ -31,24 +31,28 @@ func main() {
func newApp() *iris.Application {
app := iris.New()
app.Logger().SetLevel("debug")
ctrl := &myController{}
// Register gRPC server.
grpcServer := grpc.NewServer()
pb.RegisterGreeterServer(grpcServer, ctrl)
// Register MVC application controller.
mvc.New(app).Handle(ctrl)
// serviceName := pb.File_helloworld_proto.Services().Get(0).FullName()
// Register MVC application controller.
mvc.New(app).Handle(ctrl, mvc.GRPC{
Server: grpcServer, // Required.
ServiceName: "helloworld.Greeter", // Required.
Strict: false,
})
// Serve the gRPC server under the Iris HTTP webserver one,
// the Iris server should ran under TLS (it's a gRPC requirement).
app.WrapRouter(grpcWrapper.New(grpcServer))
return app
}
type myController struct{}
// PostHello implements helloworld.GreeterServer
func (c *myController) PostHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
// SayHello implements helloworld.GreeterServer.
func (c *myController) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}

View File

@ -10,7 +10,7 @@ func TestGRPCCompatible(t *testing.T) {
app := newApp()
e := httptest.New(t, app)
e.POST("/hello").WithJSON(map[string]string{"name": "makis"}).Expect().
e.POST("/helloworld.Greeter/SayHello").WithJSON(map[string]string{"name": "makis"}).Expect().
Status(httptest.StatusOK).
JSON().Equal(map[string]string{"message": "Hello makis"})
}

View File

@ -1,4 +0,0 @@
```sh
$ openssl genrsa -out server.key 2048
$ openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650
```

View File

@ -258,13 +258,21 @@ var WithoutAutoFireStatusCode = func(app *Application) {
app.config.DisableAutoFireStatusCode = true
}
// WithPathEscape enables the PathEscape setting.
// WithPathEscape sets the EnablePathEscape setting to true.
//
// See `Configuration`.
var WithPathEscape = func(app *Application) {
app.config.EnablePathEscape = true
}
// WithLowercaseRouting enables for lowercase routing by
// setting the `ForceLowercaseRoutes` to true.
//
// See `Configuration`.
var WithLowercaseRouting = func(app *Application) {
app.config.ForceLowercaseRouting = true
}
// WithOptimizations can force the application to optimize for the best performance where is possible.
//
// See `Configuration`.
@ -744,7 +752,8 @@ type Configuration struct {
// Defaults to false.
DisableInterruptHandler bool `json:"disableInterruptHandler,omitempty" yaml:"DisableInterruptHandler" toml:"DisableInterruptHandler"`
// DisablePathCorrection corrects and redirects or executes directly the handler of
// DisablePathCorrection disables the correcting
// and redirecting or executing directly the handler of
// the requested path to the registered path
// for example, if /home/ path is requested but no handler for this Route found,
// then the Router checks if /home handler exists, if yes,
@ -762,9 +771,7 @@ type Configuration struct {
// Defaults to false.
DisablePathCorrectionRedirection bool `json:"disablePathCorrectionRedirection,omitempty" yaml:"DisablePathCorrectionRedirection" toml:"DisablePathCorrectionRedirection"`
// EnablePathEscape when is true then its escapes the path, the named parameters (if any).
// Change to false it if you want something like this https://github.com/kataras/iris/issues/135 to work
//
// EnablePathEscape when is true then its escapes the path and the named parameters (if any).
// When do you need to Disable(false) it:
// accepts parameters with slash '/'
// Request: http://localhost:8080/details/Project%2FDelta
@ -775,6 +782,12 @@ type Configuration struct {
// Defaults to false.
EnablePathEscape bool `json:"enablePathEscape,omitempty" yaml:"EnablePathEscape" toml:"EnablePathEscape"`
// ForceLowercaseRouting if enabled, converts all registered routes paths to lowercase
// and it does lowercase the request path too for matching.
//
// Defaults to false.
ForceLowercaseRouting bool `json:"forceLowercaseRouting,omitempty" yaml:"ForceLowercaseRouting" toml:"ForceLowercaseRouting"`
// EnableOptimization when this field is true
// then the application tries to optimize for the best performance where is possible.
//
@ -900,8 +913,10 @@ func (c Configuration) GetVHost() string {
return c.vhost
}
// GetDisablePathCorrection returns the Configuration#DisablePathCorrection,
// DisablePathCorrection corrects and redirects the requested path to the registered path
// GetDisablePathCorrection returns the Configuration#DisablePathCorrection.
// DisablePathCorrection disables the correcting
// and redirecting or executing directly the handler of
// the requested path to the registered path
// for example, if /home/ path is requested but no handler for this Route found,
// then the Router checks if /home handler exists, if yes,
// (permanent)redirects the client to the correct path /home.
@ -922,6 +937,11 @@ func (c Configuration) GetEnablePathEscape() bool {
return c.EnablePathEscape
}
// GetForceLowercaseRouting returns the value of the `ForceLowercaseRouting` setting.
func (c Configuration) GetForceLowercaseRouting() bool {
return c.ForceLowercaseRouting
}
// GetEnableOptimizations returns whether
// the application has performance optimizations enabled.
func (c Configuration) GetEnableOptimizations() bool {
@ -1079,6 +1099,10 @@ func WithConfiguration(c Configuration) Configurator {
main.EnablePathEscape = v
}
if v := c.ForceLowercaseRouting; v {
main.ForceLowercaseRouting = v
}
if v := c.EnableOptimizations; v {
main.EnableOptimizations = v
}
@ -1150,6 +1174,7 @@ func DefaultConfiguration() Configuration {
DisableInterruptHandler: false,
DisablePathCorrection: false,
EnablePathEscape: false,
ForceLowercaseRouting: false,
FireMethodNotAllowed: false,
DisableBodyConsumptionOnUnmarshal: false,
DisableAutoFireStatusCode: false,

View File

@ -25,7 +25,8 @@ type ConfigurationReadOnly interface {
GetDisablePathCorrection() bool
// GetDisablePathCorrectionRedirection returns the Configuration#DisablePathCorrectionRedirection field.
// If DisablePathCorrectionRedirection set to true then it will fire the handler of the matching route without
// If DisablePathCorrectionRedirection set to true then it will handle paths as they are.
// it will fire the handler of the matching route without
// the last slash ("/") instead of send a redirection status.
GetDisablePathCorrectionRedirection() bool
@ -33,6 +34,9 @@ type ConfigurationReadOnly interface {
// returns true when its escapes the path, the named parameters (if any).
GetEnablePathEscape() bool
// GetForceLowercaseRouting returns the value of the `ForceLowercaseRouting` setting.
GetForceLowercaseRouting() bool
// GetEnableOptimizations returns whether
// the application has performance optimizations enabled.
GetEnableOptimizations() bool

View File

@ -165,7 +165,6 @@ type Context interface {
// Router is calling this function to add the route's handler.
// If AddHandler called then the handlers will be inserted
// to the end of the already-defined route's handler.
//
AddHandler(...Handler)
// SetHandlers replaces all handlers with the new.
SetHandlers(Handlers)
@ -387,6 +386,11 @@ type Context interface {
IsMobile() bool
// IsScript reports whether a client is a script.
IsScript() bool
// IsHTTP2 reports whether the protocol version for incoming request was HTTP/2.
// The client code always uses either HTTP/1.1 or HTTP/2.
IsHTTP2() bool
// IsGRPC reports whether the request came from a gRPC client.
IsGRPC() bool
// GetReferrer extracts and returns the information from the "Referer" header as specified
// in https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
// or by the URL query parameter "referer".
@ -1668,6 +1672,7 @@ func (ctx *context) RequestPath(escape bool) string {
if escape {
return ctx.request.URL.EscapedPath() // DecodeQuery(ctx.request.URL.EscapedPath())
}
return ctx.request.URL.Path // RawPath returns empty, requesturi can be used instead also.
}
@ -1826,6 +1831,17 @@ func (ctx *context) IsScript() bool {
return isScriptRegex.MatchString(s)
}
// IsHTTP2 reports whether the protocol version for incoming request was HTTP/2.
// The client code always uses either HTTP/1.1 or HTTP/2.
func (ctx *context) IsHTTP2() bool {
return ctx.Request().ProtoMajor == 2
}
// IsGRPC reports whether the request came from a gRPC client.
func (ctx *context) IsGRPC() bool {
return ctx.IsHTTP2() && ctx.GetContentTypeRequested() == ContentGRPCHeaderValue
}
type (
// Referrer contains the extracted information from the `GetReferrer`
//
@ -3316,6 +3332,8 @@ const (
ContentFormHeaderValue = "application/x-www-form-urlencoded"
// ContentFormMultipartHeaderValue header value for post multipart form data.
ContentFormMultipartHeaderValue = "multipart/form-data"
// ContentGRPCHeaderValue Content-Type header value for gRPC.
ContentGRPCHeaderValue = "application/grpc"
)
// Binary writes out the raw bytes as binary data.

View File

@ -1,6 +1,7 @@
package context
import (
"path/filepath"
"reflect"
"runtime"
"strings"
@ -27,20 +28,38 @@ type Handler func(Context)
// See `Handler` for more.
type Handlers []Handler
func valueOf(v interface{}) reflect.Value {
if val, ok := v.(reflect.Value); ok {
return val
}
return reflect.ValueOf(v)
}
// HandlerName returns the handler's function name.
// See `context.HandlerName` to get function name of the current running handler in the chain.
func HandlerName(h Handler) string {
pc := reflect.ValueOf(h).Pointer()
func HandlerName(h interface{}) string {
pc := valueOf(h).Pointer()
return runtime.FuncForPC(pc).Name()
}
// HandlerFileLine returns the handler's file and line information.
// See `context.HandlerFileLine` to get the file, line of the current running handler in the chain.
func HandlerFileLine(h Handler) (file string, line int) {
pc := reflect.ValueOf(h).Pointer()
func HandlerFileLine(h interface{}) (file string, line int) {
pc := valueOf(h).Pointer()
return runtime.FuncForPC(pc).FileLine(pc)
}
// HandlerFileLineRel same as `HandlerFileLine` but it returns the path as relative to the "workingDir".
func HandlerFileLineRel(h interface{}, workingDir string) (string, int) {
file, line := HandlerFileLine(h)
if relFile, err := filepath.Rel(workingDir, file); err == nil {
file = "./" + relFile
}
return file, line
}
// MainHandlerName tries to find the main handler than end-developer
// registered on the provided chain of handlers and returns its function name.
func MainHandlerName(handlers Handlers) (name string) {

View File

@ -163,20 +163,14 @@ var _ RoutesProvider = (*APIBuilder)(nil) // passed to the default request handl
// NewAPIBuilder creates & returns a new builder
// which is responsible to build the API and the router handler.
func NewAPIBuilder() *APIBuilder {
api := &APIBuilder{
return &APIBuilder{
macros: macro.Defaults,
errorCodeHandlers: defaultErrorCodeHandlers(),
errors: errgroup.New("API Builder"),
relativePath: "/",
routes: new(repository),
apiBuilderDI: &APIContainer{Container: hero.New()},
}
api.apiBuilderDI = &APIContainer{
Self: api,
Container: hero.New(),
}
return api
}
// ConfigureContainer accepts one or more functions that can be used
@ -187,13 +181,15 @@ func NewAPIBuilder() *APIBuilder {
//
// It returns the same `APIBuilder` featured with Dependency Injection.
func (api *APIBuilder) ConfigureContainer(builder ...func(*APIContainer)) *APIContainer {
for _, b := range builder {
if b == nil {
continue
if api.apiBuilderDI.Self == nil {
api.apiBuilderDI.Self = api
}
for _, b := range builder {
if b != nil {
b(api.apiBuilderDI)
}
}
return api.apiBuilderDI
}
@ -463,7 +459,7 @@ func (api *APIBuilder) CreateRoutes(methods []string, relativePath string, handl
subdomain, path := splitSubdomainAndPath(fullpath)
// if allowMethods are empty, then simply register with the passed, main, method.
methods = removeDuplString(append(api.allowMethods, methods...))
methods = removeDuplicates(append(api.allowMethods, methods...))
routes := make([]*Route, len(methods))
@ -487,7 +483,7 @@ func (api *APIBuilder) CreateRoutes(methods []string, relativePath string, handl
return routes
}
func removeDuplString(elements []string) (result []string) {
func removeDuplicates(elements []string) (result []string) {
seen := make(map[string]struct{})
for v := range elements {
@ -551,15 +547,11 @@ func (api *APIBuilder) Party(relativePath string, handlers ...context.Handler) P
allowMethods: allowMethods,
handlerExecutionRules: api.handlerExecutionRules,
routeRegisterRule: api.routeRegisterRule,
}
apiBuilderDI: &APIContainer{
// attach a new Container with correct dynamic path parameter start index for input arguments
// based on the fullpath.
childContainer := api.apiBuilderDI.Container.Clone()
childAPI.apiBuilderDI = &APIContainer{
Self: childAPI,
Container: childContainer,
Container: api.apiBuilderDI.Container.Clone(),
},
}
return childAPI

View File

@ -84,7 +84,7 @@ func BenchmarkAPIBuilder(b *testing.B) {
paths := genPaths(routesLength, 15, 42)
api := NewAPIBuilder()
requestHandler := NewDefaultHandler()
requestHandler := NewDefaultHandler(nil)
b.ReportAllocs()
b.ResetTimer()

View File

@ -17,7 +17,7 @@ import (
// By-default is the router algorithm.
type RequestHandler interface {
// HandleRequest should handle the request based on the Context.
HandleRequest(context.Context)
HandleRequest(ctx context.Context)
// Build should builds the handler, it's being called on router's BuildRouter.
Build(provider RoutesProvider) error
// RouteExists reports whether a particular route exists.
@ -27,6 +27,7 @@ type RequestHandler interface {
type routerHandler struct {
trees []*trie
hosts bool // true if at least one route contains a Subdomain.
config context.ConfigurationReadOnly
}
var _ RequestHandler = &routerHandler{}
@ -67,9 +68,10 @@ func (h *routerHandler) AddRoute(r *Route) error {
// NewDefaultHandler returns the handler which is responsible
// to map the request with a route (aka mux implementation).
func NewDefaultHandler() RequestHandler {
h := &routerHandler{}
return h
func NewDefaultHandler(config context.ConfigurationReadOnly) RequestHandler {
return &routerHandler{
config: config,
}
}
// RoutesProvider should be implemented by
@ -128,6 +130,11 @@ func (h *routerHandler) Build(provider RoutesProvider) error {
})
for _, r := range registeredRoutes {
if h.config != nil && h.config.GetForceLowercaseRouting() {
// only in that state, keep everyting else as end-developer registered.
r.Path = strings.ToLower(r.Path)
}
if r.Subdomain != "" {
h.hosts = true
}
@ -188,20 +195,21 @@ func bindMultiParamTypesHandler(top *Route, r *Route) {
func (h *routerHandler) HandleRequest(ctx context.Context) {
method := ctx.Method()
path := ctx.Path()
if !ctx.Application().ConfigurationReadOnly().GetDisablePathCorrection() {
config := h.config // ctx.Application().GetConfigurationReadOnly()
if !config.GetDisablePathCorrection() {
if len(path) > 1 && strings.HasSuffix(path, "/") {
// Remove trailing slash and client-permanent rule for redirection,
// if confgiuration allows that and path has an extra slash.
// update the new path and redirect.
r := ctx.Request()
u := ctx.Request().URL
// use Trim to ensure there is no open redirect due to two leading slashes
path = "/" + strings.Trim(path, "/")
r.URL.Path = path
if !ctx.Application().ConfigurationReadOnly().GetDisablePathCorrectionRedirection() {
u.Path = path
if !config.GetDisablePathCorrectionRedirection() {
// do redirect, else continue with the modified path without the last "/".
url := r.URL.String()
url := u.String()
// Fixes https://github.com/kataras/iris/issues/921
// This is caused for security reasons, imagine a payment shop,
@ -238,7 +246,7 @@ func (h *routerHandler) HandleRequest(ctx context.Context) {
// localhost -> invalid
// sub.mydomain.com -> valid
// sub.localhost -> valid
serverHost := ctx.Application().ConfigurationReadOnly().GetVHost()
serverHost := config.GetVHost()
if serverHost == requestHost {
continue // it's not a subdomain, it's a full domain (with .com...)
}
@ -266,7 +274,7 @@ func (h *routerHandler) HandleRequest(ctx context.Context) {
break
}
if ctx.Application().ConfigurationReadOnly().GetFireMethodNotAllowed() {
if config.GetFireMethodNotAllowed() {
for i := range h.trees {
t := h.trees[i]
// if `Configuration#FireMethodNotAllowed` is kept as defaulted(false) then this function will not

View File

@ -15,6 +15,7 @@ import (
// caller should Refresh the router.
type Route struct {
Name string `json:"name"` // "userRoute"
Description string `json:"description"` // "lists a user"
Method string `json:"method"` // "GET"
methodBckp string // if Method changed to something else (which is possible at runtime as well, via RefreshRouter) then this field will be filled with the old one.
Subdomain string `json:"subdomain"` // "admin."
@ -324,6 +325,10 @@ func (r *Route) Trace() string {
}
printfmt += fmt.Sprintf(" %s", r.Tmpl().Src)
if r.Description != "" {
printfmt += fmt.Sprintf(" (%s)", r.Description)
}
mainHandlerName := r.MainHandlerName
if !strings.HasSuffix(mainHandlerName, ")") {
mainHandlerName += "()"

View File

@ -1,6 +1,8 @@
package router_test
import (
"net/http"
"strings"
"testing"
"github.com/kataras/iris/v12"
@ -39,3 +41,34 @@ func TestRouteExists(t *testing.T) {
// run the tests
httptest.New(t, app, httptest.Debug(false)).Request("GET", "/route-test").Expect().Status(iris.StatusOK)
}
func TestLowercaseRouting(t *testing.T) {
app := iris.New()
app.WrapRouter(func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
// test bottom to begin wrapper, the last ones should execute first.
// The ones that are registered at `Build` state, after this `WrapRouter` call.
// So path should be already lowecased.
if expected, got := strings.ToLower(r.URL.Path), r.URL.Path; expected != got {
t.Fatalf("expected path: %s but got: %s", expected, got)
}
next(w, r)
})
h := func(ctx iris.Context) { ctx.WriteString(ctx.Path()) }
// Register routes.
tests := []string{"/", "/lowercase", "/UPPERCASE", "/Title", "/m1xEd2"}
for _, tt := range tests {
app.Get(tt, h)
}
app.Configure(iris.WithLowercaseRouting)
// Test routes.
e := httptest.New(t, app)
for _, tt := range tests {
s := strings.ToLower(tt)
e.GET(tt).Expect().Status(httptest.StatusOK).Body().Equal(s)
e.GET(s).Expect().Status(httptest.StatusOK).Body().Equal(s)
e.GET(strings.ToUpper(tt)).Expect().Status(httptest.StatusOK).Body().Equal(s)
}
}

12
iris.go
View File

@ -765,18 +765,24 @@ func (app *Application) Build() error {
if app.I18n.Loaded() {
// {{ tr "lang" "key" arg1 arg2 }}
app.view.AddFunc("tr", app.I18n.Tr)
app.WrapRouter(app.I18n.Wrapper())
app.Router.WrapRouter(app.I18n.Wrapper())
}
if !app.Router.Downgraded() {
// router
if err := app.tryInjectLiveReload(); err != nil {
rp.Errf("LiveReload: init: failed: %v", err)
}
if app.config.ForceLowercaseRouting {
app.Router.WrapRouter(func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
r.URL.Path = strings.ToLower(r.URL.Path)
next(w, r)
})
}
// create the request handler, the default routing handler
routerHandler := router.NewDefaultHandler()
routerHandler := router.NewDefaultHandler(app.config)
err := app.Router.BuildRouter(app.ContextPool, routerHandler, app.APIBuilder, false)
if err != nil {
rp.Err(err)

View File

@ -2,6 +2,7 @@ package mvc
import (
"fmt"
"os"
"reflect"
"strings"
@ -81,6 +82,9 @@ type ControllerActivator struct {
// true if this controller listens and serves to websocket events.
servesWebsocket bool
// true to skip the internal "activate".
activated bool
}
// NameOf returns the package name + the struct type's name,
@ -96,6 +100,14 @@ func NameOf(v interface{}) string {
}
func newControllerActivator(app *Application, controller interface{}) *ControllerActivator {
if controller == nil {
return nil
}
if c, ok := controller.(*ControllerActivator); ok {
return c
}
typ := reflect.TypeOf(controller)
c := &ControllerActivator{
@ -225,6 +237,11 @@ func (c *ControllerActivator) isReservedMethod(name string) bool {
return false
}
func (c *ControllerActivator) markAsWebsocket() {
c.servesWebsocket = true
c.attachInjector()
}
func (c *ControllerActivator) attachInjector() {
if c.injector == nil {
partyCountParams := macro.CountParams(c.app.Router.GetRelPath(), *c.app.Router.Macros())
@ -232,12 +249,18 @@ func (c *ControllerActivator) attachInjector() {
}
}
func (c *ControllerActivator) markAsWebsocket() {
c.servesWebsocket = true
c.attachInjector()
// Activated can be called to skip the internal method parsing.
func (c *ControllerActivator) Activated() bool {
b := c.activated
c.activated = true
return b
}
func (c *ControllerActivator) activate() {
if c.Activated() {
return
}
c.parseMethods()
}
@ -314,10 +337,16 @@ func (c *ControllerActivator) handleMany(method, path, funcName string, override
return nil
}
wd, _ := os.Getwd()
for _, r := range routes {
// change the main handler's name in order to respect the controller's and give
// a proper debug message.
// change the main handler's name and file:line
// in order to respect the controller's and give
// a proper debug/log message.
r.MainHandlerName = fmt.Sprintf("%s.%s", c.fullName, funcName)
if m, ok := c.Type.MethodByName(funcName); ok {
r.SourceFileName, r.SourceLineNumber = context.HandlerFileLineRel(m.Func, wd)
}
}
// add this as a reserved method name in order to

66
mvc/grpc.go Normal file
View File

@ -0,0 +1,66 @@
package mvc
import (
"net/http"
"path"
"github.com/kataras/iris/v12/context"
)
// GRPC registers a controller which serves gRPC clients.
// It accepts the controller ptr to a struct value,
// the gRPCServer itself, and a strict option which is explained below.
//
// The differences by a common controller are:
// HTTP verb: only POST (Party.AllowMethods can be used for more),
// method parsing is disabled: path is the function name as it is,
// if 'strictMode' option is true then this controller will only serve gRPC-based clients
// and fires 404 on common HTTP clients,
// otherwise HTTP clients can send and receive JSON (protos contain json struct fields by-default).
type GRPC struct {
// Server is required and should be gRPC Server derives from google's grpc package.
Server http.Handler
// ServiceName is required and should be the name of the service (used to build the gRPC route path),
// e.g. "helloworld.Greeter".
// For a controller's method of "SayHello" and ServiceName "helloworld.Greeter",
// both gRPC and common HTTP request path is: "/helloworld.Greeter/SayHello".
//
// Tip: the ServiceName can be fetched through proto's file descriptor, e.g.
// serviceName := pb.File_helloworld_proto.Services().Get(0).FullName().
ServiceName string
// When Strict option is true then this controller will only serve gRPC-based clients
// and fires 404 on common HTTP clients.
Strict bool
}
// Apply parses the controller's methods and registers gRPC handlers to the application.
func (g GRPC) Apply(c *ControllerActivator) {
defer c.Activated()
pre := func(ctx context.Context) {
if ctx.IsGRPC() { // gRPC, consumes and produces protobuf.
g.Server.ServeHTTP(ctx.ResponseWriter(), ctx.Request())
ctx.StopExecution()
return
}
if g.Strict {
ctx.NotFound()
} else {
// Allow common HTTP clients, consumes and produces JSON.
ctx.Next()
}
}
for i := 0; i < c.Type.NumMethod(); i++ {
m := c.Type.Method(i)
path := path.Join(g.ServiceName, m.Name)
if route := c.Handle(http.MethodPost, path, m.Name, pre); route != nil {
route.Description = "gRPC"
if g.Strict {
route.Description = "-only"
}
}
}
}

View File

@ -111,6 +111,15 @@ func (app *Application) Register(dependencies ...interface{}) *Application {
return app
}
// Option is an interface which does contain a single `Apply` method that accepts
// a `ControllerActivator`. It can be passed on `Application.Handle` method to
// mdoify the behavior right after the `BeforeActivation` state.
//
// See `GRPC` package-level structure too.
type Option interface {
Apply(*ControllerActivator)
}
// Handle serves a controller for the current mvc application's Router.
// It accept any custom struct which its functions will be transformed
// to routes.
@ -154,9 +163,12 @@ func (app *Application) Register(dependencies ...interface{}) *Application {
// Result or (Result, error)
// where Get is an HTTP Method func.
//
// Default behavior can be changed through second, variadic, variable "options",
// e.g. Handle(controller, GRPC {Server: grpcServer, Strict: true})
//
// Examples at: https://github.com/kataras/iris/tree/master/_examples/mvc
func (app *Application) Handle(controller interface{}) *Application {
app.handle(controller)
func (app *Application) Handle(controller interface{}, options ...Option) *Application {
app.handle(controller, options...)
return app
}
@ -195,7 +207,7 @@ func (app *Application) GetNamespaces() websocket.Namespaces {
return websocket.JoinConnHandlers(app.websocketControllers...).GetNamespaces()
}
func (app *Application) handle(controller interface{}) *ControllerActivator {
func (app *Application) handle(controller interface{}, options ...Option) *ControllerActivator {
// initialize the controller's activator, nothing too magical so far.
c := newControllerActivator(app, controller)
@ -208,6 +220,12 @@ func (app *Application) handle(controller interface{}) *ControllerActivator {
before.BeforeActivation(c)
}
for _, opt := range options {
if opt != nil {
opt.Apply(c)
}
}
c.activate()
if after, okAfter := controller.(interface {