diff --git a/client_options.go b/client_options.go index 32400001..36d57ac6 100644 --- a/client_options.go +++ b/client_options.go @@ -15,37 +15,51 @@ package twirp import ( "context" "net/http" + "reflect" ) // ClientOption is a functional option for extending a Twirp client. type ClientOption func(*ClientOptions) -// ClientOptions encapsulate the configurable parameters on a Twirp client. -type ClientOptions struct { - Interceptors []Interceptor - Hooks *ClientHooks - pathPrefix *string - LiteralURLs bool -} - -func (opts *ClientOptions) PathPrefix() string { - if opts.pathPrefix == nil { - return "/twirp" // default prefix +// WithClientHooks defines the hooks for a Twirp client. +func WithClientHooks(hooks *ClientHooks) ClientOption { + return func(opts *ClientOptions) { + opts.Hooks = hooks } - return *opts.pathPrefix } // WithClientInterceptors defines the interceptors for a Twirp client. func WithClientInterceptors(interceptors ...Interceptor) ClientOption { - return func(o *ClientOptions) { - o.Interceptors = append(o.Interceptors, interceptors...) + return func(opts *ClientOptions) { + opts.Interceptors = append(opts.Interceptors, interceptors...) } } -// WithClientHooks defines the hooks for a Twirp client. -func WithClientHooks(hooks *ClientHooks) ClientOption { - return func(o *ClientOptions) { - o.Hooks = hooks +// WithClientPathPrefix specifies a different prefix to use for routing. +// If not specified, the "/twirp" prefix is used by default. +// The service must be configured to serve on the same prefix. +// An empty value "" can be speficied to use no prefix. +// URL format: "[]/./" +// More info on Twirp docs: https://twitchtv.github.io/twirp/docs/routing.html +func WithClientPathPrefix(prefix string) ClientOption { + return func(opts *ClientOptions) { + opts.setOpt("pathPrefix", prefix) + opts.pathPrefix = &prefix // for code generated before v8.1.0 + } +} + +// WithClientLiteralURLs configures the Twirp client to use the exact values +// as defined in the proto file for Service and Method names, +// fixing the issue https://github.com/twitchtv/twirp/issues/244, which is manifested +// when working with Twirp services implemented other languages (e.g. Python) and the proto file definitions +// are not properly following the [Protobuf Style Guide](https://developers.google.com/protocol-buffers/docs/style#services). +// By default (false), Go clients modify the routes by CamelCasing the values. For example, +// with Service: `haberdasher`, Method: `make_hat`, the URLs generated by Go clients are `Haberdasher/MakeHat`, +// but with this option enabled (true) the client will properly use `haberdasher/make_hat` instead. +func WithClientLiteralURLs(b bool) ClientOption { + return func(opts *ClientOptions) { + opts.setOpt("literalURLs", b) + opts.LiteralURLs = b // for code generated before v8.1.0 } } @@ -120,28 +134,63 @@ func ChainClientHooks(hooks ...*ClientHooks) *ClientHooks { } } -// WithClientPathPrefix specifies a different prefix to use for routing. -// If not specified, the "/twirp" prefix is used by default. -// The service must be configured to serve on the same prefix. -// An empty value "" can be speficied to use no prefix. -// URL format: "[]/./" -// More info on Twirp docs: https://twitchtv.github.io/twirp/docs/routing.html -func WithClientPathPrefix(prefix string) ClientOption { - return func(o *ClientOptions) { - o.pathPrefix = &prefix +// ClientOptions encapsulate the configurable parameters on a Twirp client. +// This type is meant to be used only by generated code. +type ClientOptions struct { + // Untyped options map. The methods setOpt and ReadOpt are used to set + // and read options. The options are untyped so when a new option is added, + // newly generated code can still work with older versions of the runtime. + m map[string]interface{} + + Hooks *ClientHooks + Interceptors []Interceptor + + // Properties below are only used by code that was + // generated by older versions of Twirp (before v8.1.0). + // New options with standard types added in the future + // don't need new properties, they should use ReadOpt. + LiteralURLs bool + pathPrefix *string +} + +// ReadOpt extracts an option to a pointer value, +// returns true if the option exists and was extracted. +// This method is meant to be used by generated code, +// keeping the type dependency outside of the runtime. +// +// Usage example: +// +// opts.setOpt("fooOpt", 123) +// var foo int +// ok := opts.ReadOpt("fooOpt", &int) +// +func (opts *ClientOptions) ReadOpt(key string, out interface{}) bool { + val, ok := opts.m[key] + if !ok { + return false + } + + rout := reflect.ValueOf(out) + if rout.Kind() != reflect.Ptr { + panic("ReadOpt(key, out); out must be a pointer but it was not") } + rout.Elem().Set(reflect.ValueOf(val)) + return true } -// WithClientLiteralURLs configures the Twirp client to use the exact values -// as defined in the proto file for Service and Method names, -// fixing the issue https://github.com/twitchtv/twirp/issues/244, which is manifested -// when working with Twirp services implemented other languages (e.g. Python) and the proto file definitions -// are not properly following the [Protobuf Style Guide](https://developers.google.com/protocol-buffers/docs/style#services). -// By default (false), Go clients modify the routes by CamelCasing the values. For example, -// with Service: `haberdasher`, Method: `make_hat`, the URLs generated by Go clients are `Haberdasher/MakeHat`, -// but with this option enabled (true) the client will properly use `haberdasher/make_hat` instead. -func WithClientLiteralURLs(b bool) ClientOption { - return func(o *ClientOptions) { - o.LiteralURLs = b +// setOpt adds an option key/value. It is used by ServerOption helpers. +// The value can be extracted with ReadOpt by passing a pointer to the same type. +func (opts *ClientOptions) setOpt(key string, val interface{}) { + if opts.m == nil { + opts.m = make(map[string]interface{}) + } + opts.m[key] = val +} + +// PathPrefix() is used only by clients generated before v8.1.0 +func (opts *ClientOptions) PathPrefix() string { + if opts.pathPrefix == nil { + return "/twirp" // default prefix } + return *opts.pathPrefix } diff --git a/client_options_test.go b/client_options_test.go index 3028c2d7..efc42277 100644 --- a/client_options_test.go +++ b/client_options_test.go @@ -20,6 +20,41 @@ import ( "testing" ) +func TestClientOptionsReadOpt(t *testing.T) { + opts := &ClientOptions{} + ok := false + + var fooNum int + ok = opts.ReadOpt("fooNum", &fooNum) + if ok { + t.Errorf("option 'fooNum' does not exist, opts.ReadOpt should have returned false") + } + + opts.setOpt("fooNum", 455) + ok = opts.ReadOpt("fooNum", &fooNum) + if !ok || fooNum != 455 { + t.Errorf("option 'fooNum' expected to be 455") + } + + var literalURLs bool + ok = opts.ReadOpt("literalURLs", &literalURLs) + if ok { + t.Errorf("option 'literalURLs' does not exist, opts.ReadOpt should have returned false") + } + + WithClientLiteralURLs(true)(opts) + ok = opts.ReadOpt("literalURLs", &literalURLs) + if !ok || !literalURLs { + t.Errorf("option 'literalURLs' expected to be true, ok: %v, val: %v", ok, literalURLs) + } + + WithClientLiteralURLs(false)(opts) + ok = opts.ReadOpt("literalURLs", &literalURLs) + if !ok || literalURLs { + t.Errorf("option 'literalURLs' expected to be false, ok: %v, val: %v", ok, literalURLs) + } +} + func TestChainClientHooks(t *testing.T) { var ( hook1 = new(ClientHooks) diff --git a/clientcompat/internal/clientcompat/clientcompat.twirp.go b/clientcompat/internal/clientcompat/clientcompat.twirp.go index 8c179391..e3b48339 100644 --- a/clientcompat/internal/clientcompat/clientcompat.twirp.go +++ b/clientcompat/internal/clientcompat/clientcompat.twirp.go @@ -67,9 +67,17 @@ func NewCompatServiceProtobufClient(baseURL string, client HTTPClient, opts ...t o(&clientOpts) } + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + // Build method URLs: []/./ serviceURL := sanitizeBaseURL(baseURL) - serviceURL += baseServicePath(clientOpts.PathPrefix(), "twirp.clientcompat", "CompatService") + serviceURL += baseServicePath(pathPrefix, "twirp.clientcompat", "CompatService") urls := [2]string{ serviceURL + "Method", serviceURL + "NoopMethod", @@ -198,9 +206,17 @@ func NewCompatServiceJSONClient(baseURL string, client HTTPClient, opts ...twirp o(&clientOpts) } + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + // Build method URLs: []/./ serviceURL := sanitizeBaseURL(baseURL) - serviceURL += baseServicePath(clientOpts.PathPrefix(), "twirp.clientcompat", "CompatService") + serviceURL += baseServicePath(pathPrefix, "twirp.clientcompat", "CompatService") urls := [2]string{ serviceURL + "Method", serviceURL + "NoopMethod", @@ -322,26 +338,22 @@ type compatServiceServer struct { // HTTP requests that are routed to the right method in the provided svc implementation. // The opts are twirp.ServerOption modifiers, for example twirp.WithServerHooks(hooks). func NewCompatServiceServer(svc CompatService, opts ...interface{}) TwirpServer { - serverOpts := twirp.ServerOptions{} - for _, opt := range opts { - switch o := opt.(type) { - case twirp.ServerOption: - o(&serverOpts) - case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument - twirp.WithServerHooks(o)(&serverOpts) - case nil: // backwards compatibility, allow nil value for the argument - continue - default: - panic(fmt.Sprintf("Invalid option type %T on NewCompatServiceServer", o)) - } + serverOpts := newServerOpts(opts) + + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + jsonSkipDefaults := false + _ = serverOpts.ReadOpt("jsonSkipDefaults", &jsonSkipDefaults) + var pathPrefix string + if ok := serverOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix } return &compatServiceServer{ CompatService: svc, - pathPrefix: serverOpts.PathPrefix(), - interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), hooks: serverOpts.Hooks, - jsonSkipDefaults: serverOpts.JSONSkipDefaults, + interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), + pathPrefix: pathPrefix, + jsonSkipDefaults: jsonSkipDefaults, } } @@ -364,9 +376,9 @@ func (s *compatServiceServer) handleRequestBodyError(ctx context.Context, resp h s.writeError(ctx, resp, twirp.WrapError(malformedRequestError(msg), err)) } -// CompatServicePathPrefix is a convenience constant that could used to identify URL paths. +// CompatServicePathPrefix is a convenience constant that may identify URL paths. // Should be used with caution, it only matches routes generated by Twirp Go clients, -// that add a "/twirp" prefix by default, and use CamelCase service and method names. +// with the default "/twirp" prefix and default CamelCase service and method names. // More info: https://twitchtv.github.io/twirp/docs/routing.html const CompatServicePathPrefix = "/twirp/twirp.clientcompat.CompatService/" @@ -835,6 +847,23 @@ type TwirpServer interface { PathPrefix() string } +func newServerOpts(opts []interface{}) *twirp.ServerOptions { + serverOpts := &twirp.ServerOptions{} + for _, opt := range opts { + switch o := opt.(type) { + case twirp.ServerOption: + o(serverOpts) + case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument + twirp.WithServerHooks(o)(serverOpts) + case nil: // backwards compatibility, allow nil value for the argument + continue + default: + panic(fmt.Sprintf("Invalid option type %T, please use a twirp.ServerOption", o)) + } + } + return serverOpts +} + // WriteError writes an HTTP response with a valid Twirp error format (code, msg, meta). // Useful outside of the Twirp server (e.g. http middleware), but does not trigger hooks. // If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err) diff --git a/example/service.twirp.go b/example/service.twirp.go index 0f47ddff..f6525846 100644 --- a/example/service.twirp.go +++ b/example/service.twirp.go @@ -67,9 +67,17 @@ func NewHaberdasherProtobufClient(baseURL string, client HTTPClient, opts ...twi o(&clientOpts) } + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + // Build method URLs: []/./ serviceURL := sanitizeBaseURL(baseURL) - serviceURL += baseServicePath(clientOpts.PathPrefix(), "twitch.twirp.example", "Haberdasher") + serviceURL += baseServicePath(pathPrefix, "twitch.twirp.example", "Haberdasher") urls := [1]string{ serviceURL + "MakeHat", } @@ -151,9 +159,17 @@ func NewHaberdasherJSONClient(baseURL string, client HTTPClient, opts ...twirp.C o(&clientOpts) } + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + // Build method URLs: []/./ serviceURL := sanitizeBaseURL(baseURL) - serviceURL += baseServicePath(clientOpts.PathPrefix(), "twitch.twirp.example", "Haberdasher") + serviceURL += baseServicePath(pathPrefix, "twitch.twirp.example", "Haberdasher") urls := [1]string{ serviceURL + "MakeHat", } @@ -228,26 +244,22 @@ type haberdasherServer struct { // HTTP requests that are routed to the right method in the provided svc implementation. // The opts are twirp.ServerOption modifiers, for example twirp.WithServerHooks(hooks). func NewHaberdasherServer(svc Haberdasher, opts ...interface{}) TwirpServer { - serverOpts := twirp.ServerOptions{} - for _, opt := range opts { - switch o := opt.(type) { - case twirp.ServerOption: - o(&serverOpts) - case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument - twirp.WithServerHooks(o)(&serverOpts) - case nil: // backwards compatibility, allow nil value for the argument - continue - default: - panic(fmt.Sprintf("Invalid option type %T on NewHaberdasherServer", o)) - } + serverOpts := newServerOpts(opts) + + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + jsonSkipDefaults := false + _ = serverOpts.ReadOpt("jsonSkipDefaults", &jsonSkipDefaults) + var pathPrefix string + if ok := serverOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix } return &haberdasherServer{ Haberdasher: svc, - pathPrefix: serverOpts.PathPrefix(), - interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), hooks: serverOpts.Hooks, - jsonSkipDefaults: serverOpts.JSONSkipDefaults, + interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), + pathPrefix: pathPrefix, + jsonSkipDefaults: jsonSkipDefaults, } } @@ -270,9 +282,9 @@ func (s *haberdasherServer) handleRequestBodyError(ctx context.Context, resp htt s.writeError(ctx, resp, twirp.WrapError(malformedRequestError(msg), err)) } -// HaberdasherPathPrefix is a convenience constant that could used to identify URL paths. +// HaberdasherPathPrefix is a convenience constant that may identify URL paths. // Should be used with caution, it only matches routes generated by Twirp Go clients, -// that add a "/twirp" prefix by default, and use CamelCase service and method names. +// with the default "/twirp" prefix and default CamelCase service and method names. // More info: https://twitchtv.github.io/twirp/docs/routing.html const HaberdasherPathPrefix = "/twirp/twitch.twirp.example.Haberdasher/" @@ -558,6 +570,23 @@ type TwirpServer interface { PathPrefix() string } +func newServerOpts(opts []interface{}) *twirp.ServerOptions { + serverOpts := &twirp.ServerOptions{} + for _, opt := range opts { + switch o := opt.(type) { + case twirp.ServerOption: + o(serverOpts) + case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument + twirp.WithServerHooks(o)(serverOpts) + case nil: // backwards compatibility, allow nil value for the argument + continue + default: + panic(fmt.Sprintf("Invalid option type %T, please use a twirp.ServerOption", o)) + } + } + return serverOpts +} + // WriteError writes an HTTP response with a valid Twirp error format (code, msg, meta). // Useful outside of the Twirp server (e.g. http middleware), but does not trigger hooks. // If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err) diff --git a/internal/twirptest/empty_service/empty_service.twirp.go b/internal/twirptest/empty_service/empty_service.twirp.go index e573b300..cce4b5e3 100644 --- a/internal/twirptest/empty_service/empty_service.twirp.go +++ b/internal/twirptest/empty_service/empty_service.twirp.go @@ -64,6 +64,14 @@ func NewEmptyProtobufClient(baseURL string, client HTTPClient, opts ...twirp.Cli o(&clientOpts) } + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + urls := [0]string{} return &emptyProtobufClient{ @@ -97,6 +105,14 @@ func NewEmptyJSONClient(baseURL string, client HTTPClient, opts ...twirp.ClientO o(&clientOpts) } + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + urls := [0]string{} return &emptyJSONClient{ @@ -123,26 +139,22 @@ type emptyServer struct { // HTTP requests that are routed to the right method in the provided svc implementation. // The opts are twirp.ServerOption modifiers, for example twirp.WithServerHooks(hooks). func NewEmptyServer(svc Empty, opts ...interface{}) TwirpServer { - serverOpts := twirp.ServerOptions{} - for _, opt := range opts { - switch o := opt.(type) { - case twirp.ServerOption: - o(&serverOpts) - case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument - twirp.WithServerHooks(o)(&serverOpts) - case nil: // backwards compatibility, allow nil value for the argument - continue - default: - panic(fmt.Sprintf("Invalid option type %T on NewEmptyServer", o)) - } + serverOpts := newServerOpts(opts) + + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + jsonSkipDefaults := false + _ = serverOpts.ReadOpt("jsonSkipDefaults", &jsonSkipDefaults) + var pathPrefix string + if ok := serverOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix } return &emptyServer{ Empty: svc, - pathPrefix: serverOpts.PathPrefix(), - interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), hooks: serverOpts.Hooks, - jsonSkipDefaults: serverOpts.JSONSkipDefaults, + interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), + pathPrefix: pathPrefix, + jsonSkipDefaults: jsonSkipDefaults, } } @@ -165,9 +177,9 @@ func (s *emptyServer) handleRequestBodyError(ctx context.Context, resp http.Resp s.writeError(ctx, resp, twirp.WrapError(malformedRequestError(msg), err)) } -// EmptyPathPrefix is a convenience constant that could used to identify URL paths. +// EmptyPathPrefix is a convenience constant that may identify URL paths. // Should be used with caution, it only matches routes generated by Twirp Go clients, -// that add a "/twirp" prefix by default, and use CamelCase service and method names. +// with the default "/twirp" prefix and default CamelCase service and method names. // More info: https://twitchtv.github.io/twirp/docs/routing.html const EmptyPathPrefix = "/twirp/twirp.internal.twirptest.emptyservice.Empty/" @@ -270,6 +282,23 @@ type TwirpServer interface { PathPrefix() string } +func newServerOpts(opts []interface{}) *twirp.ServerOptions { + serverOpts := &twirp.ServerOptions{} + for _, opt := range opts { + switch o := opt.(type) { + case twirp.ServerOption: + o(serverOpts) + case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument + twirp.WithServerHooks(o)(serverOpts) + case nil: // backwards compatibility, allow nil value for the argument + continue + default: + panic(fmt.Sprintf("Invalid option type %T, please use a twirp.ServerOption", o)) + } + } + return serverOpts +} + // WriteError writes an HTTP response with a valid Twirp error format (code, msg, meta). // Useful outside of the Twirp server (e.g. http middleware), but does not trigger hooks. // If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err) diff --git a/internal/twirptest/google_protobuf_imports/service.twirp.go b/internal/twirptest/google_protobuf_imports/service.twirp.go index 4586fc2f..f70200e0 100644 --- a/internal/twirptest/google_protobuf_imports/service.twirp.go +++ b/internal/twirptest/google_protobuf_imports/service.twirp.go @@ -23,8 +23,8 @@ import proto "google.golang.org/protobuf/proto" import twirp "github.com/twitchtv/twirp" import ctxsetters "github.com/twitchtv/twirp/ctxsetters" -import google_protobuf1 "google.golang.org/protobuf/types/known/wrapperspb" import google_protobuf "google.golang.org/protobuf/types/known/emptypb" +import google_protobuf1 "google.golang.org/protobuf/types/known/wrapperspb" import bytes "bytes" import io "io" @@ -68,9 +68,17 @@ func NewSvcProtobufClient(baseURL string, client HTTPClient, opts ...twirp.Clien o(&clientOpts) } + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + // Build method URLs: []/./ serviceURL := sanitizeBaseURL(baseURL) - serviceURL += baseServicePath(clientOpts.PathPrefix(), "twirp.internal.twirptest.use_empty", "Svc") + serviceURL += baseServicePath(pathPrefix, "twirp.internal.twirptest.use_empty", "Svc") urls := [1]string{ serviceURL + "Send", } @@ -152,9 +160,17 @@ func NewSvcJSONClient(baseURL string, client HTTPClient, opts ...twirp.ClientOpt o(&clientOpts) } + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + // Build method URLs: []/./ serviceURL := sanitizeBaseURL(baseURL) - serviceURL += baseServicePath(clientOpts.PathPrefix(), "twirp.internal.twirptest.use_empty", "Svc") + serviceURL += baseServicePath(pathPrefix, "twirp.internal.twirptest.use_empty", "Svc") urls := [1]string{ serviceURL + "Send", } @@ -229,26 +245,22 @@ type svcServer struct { // HTTP requests that are routed to the right method in the provided svc implementation. // The opts are twirp.ServerOption modifiers, for example twirp.WithServerHooks(hooks). func NewSvcServer(svc Svc, opts ...interface{}) TwirpServer { - serverOpts := twirp.ServerOptions{} - for _, opt := range opts { - switch o := opt.(type) { - case twirp.ServerOption: - o(&serverOpts) - case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument - twirp.WithServerHooks(o)(&serverOpts) - case nil: // backwards compatibility, allow nil value for the argument - continue - default: - panic(fmt.Sprintf("Invalid option type %T on NewSvcServer", o)) - } + serverOpts := newServerOpts(opts) + + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + jsonSkipDefaults := false + _ = serverOpts.ReadOpt("jsonSkipDefaults", &jsonSkipDefaults) + var pathPrefix string + if ok := serverOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix } return &svcServer{ Svc: svc, - pathPrefix: serverOpts.PathPrefix(), - interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), hooks: serverOpts.Hooks, - jsonSkipDefaults: serverOpts.JSONSkipDefaults, + interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), + pathPrefix: pathPrefix, + jsonSkipDefaults: jsonSkipDefaults, } } @@ -271,9 +283,9 @@ func (s *svcServer) handleRequestBodyError(ctx context.Context, resp http.Respon s.writeError(ctx, resp, twirp.WrapError(malformedRequestError(msg), err)) } -// SvcPathPrefix is a convenience constant that could used to identify URL paths. +// SvcPathPrefix is a convenience constant that may identify URL paths. // Should be used with caution, it only matches routes generated by Twirp Go clients, -// that add a "/twirp" prefix by default, and use CamelCase service and method names. +// with the default "/twirp" prefix and default CamelCase service and method names. // More info: https://twitchtv.github.io/twirp/docs/routing.html const SvcPathPrefix = "/twirp/twirp.internal.twirptest.use_empty.Svc/" @@ -559,6 +571,23 @@ type TwirpServer interface { PathPrefix() string } +func newServerOpts(opts []interface{}) *twirp.ServerOptions { + serverOpts := &twirp.ServerOptions{} + for _, opt := range opts { + switch o := opt.(type) { + case twirp.ServerOption: + o(serverOpts) + case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument + twirp.WithServerHooks(o)(serverOpts) + case nil: // backwards compatibility, allow nil value for the argument + continue + default: + panic(fmt.Sprintf("Invalid option type %T, please use a twirp.ServerOption", o)) + } + } + return serverOpts +} + // WriteError writes an HTTP response with a valid Twirp error format (code, msg, meta). // Useful outside of the Twirp server (e.g. http middleware), but does not trigger hooks. // If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err) diff --git a/internal/twirptest/importable/importable.twirp.go b/internal/twirptest/importable/importable.twirp.go index 403ff0e6..016f286b 100644 --- a/internal/twirptest/importable/importable.twirp.go +++ b/internal/twirptest/importable/importable.twirp.go @@ -68,9 +68,17 @@ func NewSvcProtobufClient(baseURL string, client HTTPClient, opts ...twirp.Clien o(&clientOpts) } + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + // Build method URLs: []/./ serviceURL := sanitizeBaseURL(baseURL) - serviceURL += baseServicePath(clientOpts.PathPrefix(), "twirp.internal.twirptest.importable", "Svc") + serviceURL += baseServicePath(pathPrefix, "twirp.internal.twirptest.importable", "Svc") urls := [1]string{ serviceURL + "Send", } @@ -152,9 +160,17 @@ func NewSvcJSONClient(baseURL string, client HTTPClient, opts ...twirp.ClientOpt o(&clientOpts) } + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + // Build method URLs: []/./ serviceURL := sanitizeBaseURL(baseURL) - serviceURL += baseServicePath(clientOpts.PathPrefix(), "twirp.internal.twirptest.importable", "Svc") + serviceURL += baseServicePath(pathPrefix, "twirp.internal.twirptest.importable", "Svc") urls := [1]string{ serviceURL + "Send", } @@ -229,26 +245,22 @@ type svcServer struct { // HTTP requests that are routed to the right method in the provided svc implementation. // The opts are twirp.ServerOption modifiers, for example twirp.WithServerHooks(hooks). func NewSvcServer(svc Svc, opts ...interface{}) TwirpServer { - serverOpts := twirp.ServerOptions{} - for _, opt := range opts { - switch o := opt.(type) { - case twirp.ServerOption: - o(&serverOpts) - case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument - twirp.WithServerHooks(o)(&serverOpts) - case nil: // backwards compatibility, allow nil value for the argument - continue - default: - panic(fmt.Sprintf("Invalid option type %T on NewSvcServer", o)) - } + serverOpts := newServerOpts(opts) + + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + jsonSkipDefaults := false + _ = serverOpts.ReadOpt("jsonSkipDefaults", &jsonSkipDefaults) + var pathPrefix string + if ok := serverOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix } return &svcServer{ Svc: svc, - pathPrefix: serverOpts.PathPrefix(), - interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), hooks: serverOpts.Hooks, - jsonSkipDefaults: serverOpts.JSONSkipDefaults, + interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), + pathPrefix: pathPrefix, + jsonSkipDefaults: jsonSkipDefaults, } } @@ -271,9 +283,9 @@ func (s *svcServer) handleRequestBodyError(ctx context.Context, resp http.Respon s.writeError(ctx, resp, twirp.WrapError(malformedRequestError(msg), err)) } -// SvcPathPrefix is a convenience constant that could used to identify URL paths. +// SvcPathPrefix is a convenience constant that may identify URL paths. // Should be used with caution, it only matches routes generated by Twirp Go clients, -// that add a "/twirp" prefix by default, and use CamelCase service and method names. +// with the default "/twirp" prefix and default CamelCase service and method names. // More info: https://twitchtv.github.io/twirp/docs/routing.html const SvcPathPrefix = "/twirp/twirp.internal.twirptest.importable.Svc/" @@ -559,6 +571,23 @@ type TwirpServer interface { PathPrefix() string } +func newServerOpts(opts []interface{}) *twirp.ServerOptions { + serverOpts := &twirp.ServerOptions{} + for _, opt := range opts { + switch o := opt.(type) { + case twirp.ServerOption: + o(serverOpts) + case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument + twirp.WithServerHooks(o)(serverOpts) + case nil: // backwards compatibility, allow nil value for the argument + continue + default: + panic(fmt.Sprintf("Invalid option type %T, please use a twirp.ServerOption", o)) + } + } + return serverOpts +} + // WriteError writes an HTTP response with a valid Twirp error format (code, msg, meta). // Useful outside of the Twirp server (e.g. http middleware), but does not trigger hooks. // If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err) diff --git a/internal/twirptest/importer/importer.twirp.go b/internal/twirptest/importer/importer.twirp.go index 38697daa..9ac6fa95 100644 --- a/internal/twirptest/importer/importer.twirp.go +++ b/internal/twirptest/importer/importer.twirp.go @@ -70,9 +70,17 @@ func NewSvc2ProtobufClient(baseURL string, client HTTPClient, opts ...twirp.Clie o(&clientOpts) } + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + // Build method URLs: []/./ serviceURL := sanitizeBaseURL(baseURL) - serviceURL += baseServicePath(clientOpts.PathPrefix(), "twirp.internal.twirptest.importer", "Svc2") + serviceURL += baseServicePath(pathPrefix, "twirp.internal.twirptest.importer", "Svc2") urls := [1]string{ serviceURL + "Send", } @@ -154,9 +162,17 @@ func NewSvc2JSONClient(baseURL string, client HTTPClient, opts ...twirp.ClientOp o(&clientOpts) } + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + // Build method URLs: []/./ serviceURL := sanitizeBaseURL(baseURL) - serviceURL += baseServicePath(clientOpts.PathPrefix(), "twirp.internal.twirptest.importer", "Svc2") + serviceURL += baseServicePath(pathPrefix, "twirp.internal.twirptest.importer", "Svc2") urls := [1]string{ serviceURL + "Send", } @@ -231,26 +247,22 @@ type svc2Server struct { // HTTP requests that are routed to the right method in the provided svc implementation. // The opts are twirp.ServerOption modifiers, for example twirp.WithServerHooks(hooks). func NewSvc2Server(svc Svc2, opts ...interface{}) TwirpServer { - serverOpts := twirp.ServerOptions{} - for _, opt := range opts { - switch o := opt.(type) { - case twirp.ServerOption: - o(&serverOpts) - case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument - twirp.WithServerHooks(o)(&serverOpts) - case nil: // backwards compatibility, allow nil value for the argument - continue - default: - panic(fmt.Sprintf("Invalid option type %T on NewSvc2Server", o)) - } + serverOpts := newServerOpts(opts) + + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + jsonSkipDefaults := false + _ = serverOpts.ReadOpt("jsonSkipDefaults", &jsonSkipDefaults) + var pathPrefix string + if ok := serverOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix } return &svc2Server{ Svc2: svc, - pathPrefix: serverOpts.PathPrefix(), - interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), hooks: serverOpts.Hooks, - jsonSkipDefaults: serverOpts.JSONSkipDefaults, + interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), + pathPrefix: pathPrefix, + jsonSkipDefaults: jsonSkipDefaults, } } @@ -273,9 +285,9 @@ func (s *svc2Server) handleRequestBodyError(ctx context.Context, resp http.Respo s.writeError(ctx, resp, twirp.WrapError(malformedRequestError(msg), err)) } -// Svc2PathPrefix is a convenience constant that could used to identify URL paths. +// Svc2PathPrefix is a convenience constant that may identify URL paths. // Should be used with caution, it only matches routes generated by Twirp Go clients, -// that add a "/twirp" prefix by default, and use CamelCase service and method names. +// with the default "/twirp" prefix and default CamelCase service and method names. // More info: https://twitchtv.github.io/twirp/docs/routing.html const Svc2PathPrefix = "/twirp/twirp.internal.twirptest.importer.Svc2/" @@ -561,6 +573,23 @@ type TwirpServer interface { PathPrefix() string } +func newServerOpts(opts []interface{}) *twirp.ServerOptions { + serverOpts := &twirp.ServerOptions{} + for _, opt := range opts { + switch o := opt.(type) { + case twirp.ServerOption: + o(serverOpts) + case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument + twirp.WithServerHooks(o)(serverOpts) + case nil: // backwards compatibility, allow nil value for the argument + continue + default: + panic(fmt.Sprintf("Invalid option type %T, please use a twirp.ServerOption", o)) + } + } + return serverOpts +} + // WriteError writes an HTTP response with a valid Twirp error format (code, msg, meta). // Useful outside of the Twirp server (e.g. http middleware), but does not trigger hooks. // If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err) diff --git a/internal/twirptest/importer_local/importer_local.twirp.go b/internal/twirptest/importer_local/importer_local.twirp.go index 7a995575..92d9e7c5 100644 --- a/internal/twirptest/importer_local/importer_local.twirp.go +++ b/internal/twirptest/importer_local/importer_local.twirp.go @@ -65,9 +65,17 @@ func NewSvcProtobufClient(baseURL string, client HTTPClient, opts ...twirp.Clien o(&clientOpts) } + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + // Build method URLs: []/./ serviceURL := sanitizeBaseURL(baseURL) - serviceURL += baseServicePath(clientOpts.PathPrefix(), "twirp.internal.twirptest.importer_local", "Svc") + serviceURL += baseServicePath(pathPrefix, "twirp.internal.twirptest.importer_local", "Svc") urls := [1]string{ serviceURL + "Send", } @@ -149,9 +157,17 @@ func NewSvcJSONClient(baseURL string, client HTTPClient, opts ...twirp.ClientOpt o(&clientOpts) } + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + // Build method URLs: []/./ serviceURL := sanitizeBaseURL(baseURL) - serviceURL += baseServicePath(clientOpts.PathPrefix(), "twirp.internal.twirptest.importer_local", "Svc") + serviceURL += baseServicePath(pathPrefix, "twirp.internal.twirptest.importer_local", "Svc") urls := [1]string{ serviceURL + "Send", } @@ -226,26 +242,22 @@ type svcServer struct { // HTTP requests that are routed to the right method in the provided svc implementation. // The opts are twirp.ServerOption modifiers, for example twirp.WithServerHooks(hooks). func NewSvcServer(svc Svc, opts ...interface{}) TwirpServer { - serverOpts := twirp.ServerOptions{} - for _, opt := range opts { - switch o := opt.(type) { - case twirp.ServerOption: - o(&serverOpts) - case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument - twirp.WithServerHooks(o)(&serverOpts) - case nil: // backwards compatibility, allow nil value for the argument - continue - default: - panic(fmt.Sprintf("Invalid option type %T on NewSvcServer", o)) - } + serverOpts := newServerOpts(opts) + + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + jsonSkipDefaults := false + _ = serverOpts.ReadOpt("jsonSkipDefaults", &jsonSkipDefaults) + var pathPrefix string + if ok := serverOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix } return &svcServer{ Svc: svc, - pathPrefix: serverOpts.PathPrefix(), - interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), hooks: serverOpts.Hooks, - jsonSkipDefaults: serverOpts.JSONSkipDefaults, + interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), + pathPrefix: pathPrefix, + jsonSkipDefaults: jsonSkipDefaults, } } @@ -268,9 +280,9 @@ func (s *svcServer) handleRequestBodyError(ctx context.Context, resp http.Respon s.writeError(ctx, resp, twirp.WrapError(malformedRequestError(msg), err)) } -// SvcPathPrefix is a convenience constant that could used to identify URL paths. +// SvcPathPrefix is a convenience constant that may identify URL paths. // Should be used with caution, it only matches routes generated by Twirp Go clients, -// that add a "/twirp" prefix by default, and use CamelCase service and method names. +// with the default "/twirp" prefix and default CamelCase service and method names. // More info: https://twitchtv.github.io/twirp/docs/routing.html const SvcPathPrefix = "/twirp/twirp.internal.twirptest.importer_local.Svc/" @@ -556,6 +568,23 @@ type TwirpServer interface { PathPrefix() string } +func newServerOpts(opts []interface{}) *twirp.ServerOptions { + serverOpts := &twirp.ServerOptions{} + for _, opt := range opts { + switch o := opt.(type) { + case twirp.ServerOption: + o(serverOpts) + case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument + twirp.WithServerHooks(o)(serverOpts) + case nil: // backwards compatibility, allow nil value for the argument + continue + default: + panic(fmt.Sprintf("Invalid option type %T, please use a twirp.ServerOption", o)) + } + } + return serverOpts +} + // WriteError writes an HTTP response with a valid Twirp error format (code, msg, meta). // Useful outside of the Twirp server (e.g. http middleware), but does not trigger hooks. // If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err) diff --git a/internal/twirptest/importmapping/x/x.twirp.go b/internal/twirptest/importmapping/x/x.twirp.go index bcad85e9..32668f5a 100644 --- a/internal/twirptest/importmapping/x/x.twirp.go +++ b/internal/twirptest/importmapping/x/x.twirp.go @@ -67,9 +67,17 @@ func NewSvc1ProtobufClient(baseURL string, client HTTPClient, opts ...twirp.Clie o(&clientOpts) } + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + // Build method URLs: []/./ serviceURL := sanitizeBaseURL(baseURL) - serviceURL += baseServicePath(clientOpts.PathPrefix(), "twirp.internal.twirptest.importmapping.x", "Svc1") + serviceURL += baseServicePath(pathPrefix, "twirp.internal.twirptest.importmapping.x", "Svc1") urls := [1]string{ serviceURL + "Send", } @@ -151,9 +159,17 @@ func NewSvc1JSONClient(baseURL string, client HTTPClient, opts ...twirp.ClientOp o(&clientOpts) } + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + // Build method URLs: []/./ serviceURL := sanitizeBaseURL(baseURL) - serviceURL += baseServicePath(clientOpts.PathPrefix(), "twirp.internal.twirptest.importmapping.x", "Svc1") + serviceURL += baseServicePath(pathPrefix, "twirp.internal.twirptest.importmapping.x", "Svc1") urls := [1]string{ serviceURL + "Send", } @@ -228,26 +244,22 @@ type svc1Server struct { // HTTP requests that are routed to the right method in the provided svc implementation. // The opts are twirp.ServerOption modifiers, for example twirp.WithServerHooks(hooks). func NewSvc1Server(svc Svc1, opts ...interface{}) TwirpServer { - serverOpts := twirp.ServerOptions{} - for _, opt := range opts { - switch o := opt.(type) { - case twirp.ServerOption: - o(&serverOpts) - case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument - twirp.WithServerHooks(o)(&serverOpts) - case nil: // backwards compatibility, allow nil value for the argument - continue - default: - panic(fmt.Sprintf("Invalid option type %T on NewSvc1Server", o)) - } + serverOpts := newServerOpts(opts) + + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + jsonSkipDefaults := false + _ = serverOpts.ReadOpt("jsonSkipDefaults", &jsonSkipDefaults) + var pathPrefix string + if ok := serverOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix } return &svc1Server{ Svc1: svc, - pathPrefix: serverOpts.PathPrefix(), - interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), hooks: serverOpts.Hooks, - jsonSkipDefaults: serverOpts.JSONSkipDefaults, + interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), + pathPrefix: pathPrefix, + jsonSkipDefaults: jsonSkipDefaults, } } @@ -270,9 +282,9 @@ func (s *svc1Server) handleRequestBodyError(ctx context.Context, resp http.Respo s.writeError(ctx, resp, twirp.WrapError(malformedRequestError(msg), err)) } -// Svc1PathPrefix is a convenience constant that could used to identify URL paths. +// Svc1PathPrefix is a convenience constant that may identify URL paths. // Should be used with caution, it only matches routes generated by Twirp Go clients, -// that add a "/twirp" prefix by default, and use CamelCase service and method names. +// with the default "/twirp" prefix and default CamelCase service and method names. // More info: https://twitchtv.github.io/twirp/docs/routing.html const Svc1PathPrefix = "/twirp/twirp.internal.twirptest.importmapping.x.Svc1/" @@ -558,6 +570,23 @@ type TwirpServer interface { PathPrefix() string } +func newServerOpts(opts []interface{}) *twirp.ServerOptions { + serverOpts := &twirp.ServerOptions{} + for _, opt := range opts { + switch o := opt.(type) { + case twirp.ServerOption: + o(serverOpts) + case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument + twirp.WithServerHooks(o)(serverOpts) + case nil: // backwards compatibility, allow nil value for the argument + continue + default: + panic(fmt.Sprintf("Invalid option type %T, please use a twirp.ServerOption", o)) + } + } + return serverOpts +} + // WriteError writes an HTTP response with a valid Twirp error format (code, msg, meta). // Useful outside of the Twirp server (e.g. http middleware), but does not trigger hooks. // If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err) diff --git a/internal/twirptest/json_serialization/json_serialization.twirp.go b/internal/twirptest/json_serialization/json_serialization.twirp.go index 0e0861f0..2a40b80e 100644 --- a/internal/twirptest/json_serialization/json_serialization.twirp.go +++ b/internal/twirptest/json_serialization/json_serialization.twirp.go @@ -65,9 +65,17 @@ func NewJSONSerializationProtobufClient(baseURL string, client HTTPClient, opts o(&clientOpts) } + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + // Build method URLs: []/./ serviceURL := sanitizeBaseURL(baseURL) - serviceURL += baseServicePath(clientOpts.PathPrefix(), "", "JSONSerialization") + serviceURL += baseServicePath(pathPrefix, "", "JSONSerialization") urls := [1]string{ serviceURL + "EchoJSON", } @@ -149,9 +157,17 @@ func NewJSONSerializationJSONClient(baseURL string, client HTTPClient, opts ...t o(&clientOpts) } + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + // Build method URLs: []/./ serviceURL := sanitizeBaseURL(baseURL) - serviceURL += baseServicePath(clientOpts.PathPrefix(), "", "JSONSerialization") + serviceURL += baseServicePath(pathPrefix, "", "JSONSerialization") urls := [1]string{ serviceURL + "EchoJSON", } @@ -226,26 +242,22 @@ type jSONSerializationServer struct { // HTTP requests that are routed to the right method in the provided svc implementation. // The opts are twirp.ServerOption modifiers, for example twirp.WithServerHooks(hooks). func NewJSONSerializationServer(svc JSONSerialization, opts ...interface{}) TwirpServer { - serverOpts := twirp.ServerOptions{} - for _, opt := range opts { - switch o := opt.(type) { - case twirp.ServerOption: - o(&serverOpts) - case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument - twirp.WithServerHooks(o)(&serverOpts) - case nil: // backwards compatibility, allow nil value for the argument - continue - default: - panic(fmt.Sprintf("Invalid option type %T on NewJSONSerializationServer", o)) - } + serverOpts := newServerOpts(opts) + + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + jsonSkipDefaults := false + _ = serverOpts.ReadOpt("jsonSkipDefaults", &jsonSkipDefaults) + var pathPrefix string + if ok := serverOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix } return &jSONSerializationServer{ JSONSerialization: svc, - pathPrefix: serverOpts.PathPrefix(), - interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), hooks: serverOpts.Hooks, - jsonSkipDefaults: serverOpts.JSONSkipDefaults, + interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), + pathPrefix: pathPrefix, + jsonSkipDefaults: jsonSkipDefaults, } } @@ -268,9 +280,9 @@ func (s *jSONSerializationServer) handleRequestBodyError(ctx context.Context, re s.writeError(ctx, resp, twirp.WrapError(malformedRequestError(msg), err)) } -// JSONSerializationPathPrefix is a convenience constant that could used to identify URL paths. +// JSONSerializationPathPrefix is a convenience constant that may identify URL paths. // Should be used with caution, it only matches routes generated by Twirp Go clients, -// that add a "/twirp" prefix by default, and use CamelCase service and method names. +// with the default "/twirp" prefix and default CamelCase service and method names. // More info: https://twitchtv.github.io/twirp/docs/routing.html const JSONSerializationPathPrefix = "/twirp/JSONSerialization/" @@ -556,6 +568,23 @@ type TwirpServer interface { PathPrefix() string } +func newServerOpts(opts []interface{}) *twirp.ServerOptions { + serverOpts := &twirp.ServerOptions{} + for _, opt := range opts { + switch o := opt.(type) { + case twirp.ServerOption: + o(serverOpts) + case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument + twirp.WithServerHooks(o)(serverOpts) + case nil: // backwards compatibility, allow nil value for the argument + continue + default: + panic(fmt.Sprintf("Invalid option type %T, please use a twirp.ServerOption", o)) + } + } + return serverOpts +} + // WriteError writes an HTTP response with a valid Twirp error format (code, msg, meta). // Useful outside of the Twirp server (e.g. http middleware), but does not trigger hooks. // If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err) diff --git a/internal/twirptest/multiple/multiple1.twirp.go b/internal/twirptest/multiple/multiple1.twirp.go index ec8824b8..38b317b3 100644 --- a/internal/twirptest/multiple/multiple1.twirp.go +++ b/internal/twirptest/multiple/multiple1.twirp.go @@ -69,9 +69,17 @@ func NewSvc1ProtobufClient(baseURL string, client HTTPClient, opts ...twirp.Clie o(&clientOpts) } + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + // Build method URLs: []/./ serviceURL := sanitizeBaseURL(baseURL) - serviceURL += baseServicePath(clientOpts.PathPrefix(), "twirp.internal.twirptest.multiple", "Svc1") + serviceURL += baseServicePath(pathPrefix, "twirp.internal.twirptest.multiple", "Svc1") urls := [1]string{ serviceURL + "Send", } @@ -153,9 +161,17 @@ func NewSvc1JSONClient(baseURL string, client HTTPClient, opts ...twirp.ClientOp o(&clientOpts) } + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + // Build method URLs: []/./ serviceURL := sanitizeBaseURL(baseURL) - serviceURL += baseServicePath(clientOpts.PathPrefix(), "twirp.internal.twirptest.multiple", "Svc1") + serviceURL += baseServicePath(pathPrefix, "twirp.internal.twirptest.multiple", "Svc1") urls := [1]string{ serviceURL + "Send", } @@ -230,26 +246,22 @@ type svc1Server struct { // HTTP requests that are routed to the right method in the provided svc implementation. // The opts are twirp.ServerOption modifiers, for example twirp.WithServerHooks(hooks). func NewSvc1Server(svc Svc1, opts ...interface{}) TwirpServer { - serverOpts := twirp.ServerOptions{} - for _, opt := range opts { - switch o := opt.(type) { - case twirp.ServerOption: - o(&serverOpts) - case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument - twirp.WithServerHooks(o)(&serverOpts) - case nil: // backwards compatibility, allow nil value for the argument - continue - default: - panic(fmt.Sprintf("Invalid option type %T on NewSvc1Server", o)) - } + serverOpts := newServerOpts(opts) + + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + jsonSkipDefaults := false + _ = serverOpts.ReadOpt("jsonSkipDefaults", &jsonSkipDefaults) + var pathPrefix string + if ok := serverOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix } return &svc1Server{ Svc1: svc, - pathPrefix: serverOpts.PathPrefix(), - interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), hooks: serverOpts.Hooks, - jsonSkipDefaults: serverOpts.JSONSkipDefaults, + interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), + pathPrefix: pathPrefix, + jsonSkipDefaults: jsonSkipDefaults, } } @@ -272,9 +284,9 @@ func (s *svc1Server) handleRequestBodyError(ctx context.Context, resp http.Respo s.writeError(ctx, resp, twirp.WrapError(malformedRequestError(msg), err)) } -// Svc1PathPrefix is a convenience constant that could used to identify URL paths. +// Svc1PathPrefix is a convenience constant that may identify URL paths. // Should be used with caution, it only matches routes generated by Twirp Go clients, -// that add a "/twirp" prefix by default, and use CamelCase service and method names. +// with the default "/twirp" prefix and default CamelCase service and method names. // More info: https://twitchtv.github.io/twirp/docs/routing.html const Svc1PathPrefix = "/twirp/twirp.internal.twirptest.multiple.Svc1/" @@ -560,6 +572,23 @@ type TwirpServer interface { PathPrefix() string } +func newServerOpts(opts []interface{}) *twirp.ServerOptions { + serverOpts := &twirp.ServerOptions{} + for _, opt := range opts { + switch o := opt.(type) { + case twirp.ServerOption: + o(serverOpts) + case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument + twirp.WithServerHooks(o)(serverOpts) + case nil: // backwards compatibility, allow nil value for the argument + continue + default: + panic(fmt.Sprintf("Invalid option type %T, please use a twirp.ServerOption", o)) + } + } + return serverOpts +} + // WriteError writes an HTTP response with a valid Twirp error format (code, msg, meta). // Useful outside of the Twirp server (e.g. http middleware), but does not trigger hooks. // If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err) diff --git a/internal/twirptest/multiple/multiple2.twirp.go b/internal/twirptest/multiple/multiple2.twirp.go index 36746d1d..9272bc19 100644 --- a/internal/twirptest/multiple/multiple2.twirp.go +++ b/internal/twirptest/multiple/multiple2.twirp.go @@ -55,9 +55,17 @@ func NewSvc2ProtobufClient(baseURL string, client HTTPClient, opts ...twirp.Clie o(&clientOpts) } + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + // Build method URLs: []/./ serviceURL := sanitizeBaseURL(baseURL) - serviceURL += baseServicePath(clientOpts.PathPrefix(), "twirp.internal.twirptest.multiple", "Svc2") + serviceURL += baseServicePath(pathPrefix, "twirp.internal.twirptest.multiple", "Svc2") urls := [2]string{ serviceURL + "Send", serviceURL + "SamePackageProtoImport", @@ -186,9 +194,17 @@ func NewSvc2JSONClient(baseURL string, client HTTPClient, opts ...twirp.ClientOp o(&clientOpts) } + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + // Build method URLs: []/./ serviceURL := sanitizeBaseURL(baseURL) - serviceURL += baseServicePath(clientOpts.PathPrefix(), "twirp.internal.twirptest.multiple", "Svc2") + serviceURL += baseServicePath(pathPrefix, "twirp.internal.twirptest.multiple", "Svc2") urls := [2]string{ serviceURL + "Send", serviceURL + "SamePackageProtoImport", @@ -310,26 +326,22 @@ type svc2Server struct { // HTTP requests that are routed to the right method in the provided svc implementation. // The opts are twirp.ServerOption modifiers, for example twirp.WithServerHooks(hooks). func NewSvc2Server(svc Svc2, opts ...interface{}) TwirpServer { - serverOpts := twirp.ServerOptions{} - for _, opt := range opts { - switch o := opt.(type) { - case twirp.ServerOption: - o(&serverOpts) - case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument - twirp.WithServerHooks(o)(&serverOpts) - case nil: // backwards compatibility, allow nil value for the argument - continue - default: - panic(fmt.Sprintf("Invalid option type %T on NewSvc2Server", o)) - } + serverOpts := newServerOpts(opts) + + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + jsonSkipDefaults := false + _ = serverOpts.ReadOpt("jsonSkipDefaults", &jsonSkipDefaults) + var pathPrefix string + if ok := serverOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix } return &svc2Server{ Svc2: svc, - pathPrefix: serverOpts.PathPrefix(), - interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), hooks: serverOpts.Hooks, - jsonSkipDefaults: serverOpts.JSONSkipDefaults, + interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), + pathPrefix: pathPrefix, + jsonSkipDefaults: jsonSkipDefaults, } } @@ -352,9 +364,9 @@ func (s *svc2Server) handleRequestBodyError(ctx context.Context, resp http.Respo s.writeError(ctx, resp, twirp.WrapError(malformedRequestError(msg), err)) } -// Svc2PathPrefix is a convenience constant that could used to identify URL paths. +// Svc2PathPrefix is a convenience constant that may identify URL paths. // Should be used with caution, it only matches routes generated by Twirp Go clients, -// that add a "/twirp" prefix by default, and use CamelCase service and method names. +// with the default "/twirp" prefix and default CamelCase service and method names. // More info: https://twitchtv.github.io/twirp/docs/routing.html const Svc2PathPrefix = "/twirp/twirp.internal.twirptest.multiple.Svc2/" diff --git a/internal/twirptest/no_package_name/no_package_name.twirp.go b/internal/twirptest/no_package_name/no_package_name.twirp.go index b9368fbf..f406fe4a 100644 --- a/internal/twirptest/no_package_name/no_package_name.twirp.go +++ b/internal/twirptest/no_package_name/no_package_name.twirp.go @@ -65,9 +65,17 @@ func NewSvcProtobufClient(baseURL string, client HTTPClient, opts ...twirp.Clien o(&clientOpts) } + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + // Build method URLs: []/./ serviceURL := sanitizeBaseURL(baseURL) - serviceURL += baseServicePath(clientOpts.PathPrefix(), "", "Svc") + serviceURL += baseServicePath(pathPrefix, "", "Svc") urls := [1]string{ serviceURL + "Send", } @@ -149,9 +157,17 @@ func NewSvcJSONClient(baseURL string, client HTTPClient, opts ...twirp.ClientOpt o(&clientOpts) } + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + // Build method URLs: []/./ serviceURL := sanitizeBaseURL(baseURL) - serviceURL += baseServicePath(clientOpts.PathPrefix(), "", "Svc") + serviceURL += baseServicePath(pathPrefix, "", "Svc") urls := [1]string{ serviceURL + "Send", } @@ -226,26 +242,22 @@ type svcServer struct { // HTTP requests that are routed to the right method in the provided svc implementation. // The opts are twirp.ServerOption modifiers, for example twirp.WithServerHooks(hooks). func NewSvcServer(svc Svc, opts ...interface{}) TwirpServer { - serverOpts := twirp.ServerOptions{} - for _, opt := range opts { - switch o := opt.(type) { - case twirp.ServerOption: - o(&serverOpts) - case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument - twirp.WithServerHooks(o)(&serverOpts) - case nil: // backwards compatibility, allow nil value for the argument - continue - default: - panic(fmt.Sprintf("Invalid option type %T on NewSvcServer", o)) - } + serverOpts := newServerOpts(opts) + + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + jsonSkipDefaults := false + _ = serverOpts.ReadOpt("jsonSkipDefaults", &jsonSkipDefaults) + var pathPrefix string + if ok := serverOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix } return &svcServer{ Svc: svc, - pathPrefix: serverOpts.PathPrefix(), - interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), hooks: serverOpts.Hooks, - jsonSkipDefaults: serverOpts.JSONSkipDefaults, + interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), + pathPrefix: pathPrefix, + jsonSkipDefaults: jsonSkipDefaults, } } @@ -268,9 +280,9 @@ func (s *svcServer) handleRequestBodyError(ctx context.Context, resp http.Respon s.writeError(ctx, resp, twirp.WrapError(malformedRequestError(msg), err)) } -// SvcPathPrefix is a convenience constant that could used to identify URL paths. +// SvcPathPrefix is a convenience constant that may identify URL paths. // Should be used with caution, it only matches routes generated by Twirp Go clients, -// that add a "/twirp" prefix by default, and use CamelCase service and method names. +// with the default "/twirp" prefix and default CamelCase service and method names. // More info: https://twitchtv.github.io/twirp/docs/routing.html const SvcPathPrefix = "/twirp/Svc/" @@ -556,6 +568,23 @@ type TwirpServer interface { PathPrefix() string } +func newServerOpts(opts []interface{}) *twirp.ServerOptions { + serverOpts := &twirp.ServerOptions{} + for _, opt := range opts { + switch o := opt.(type) { + case twirp.ServerOption: + o(serverOpts) + case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument + twirp.WithServerHooks(o)(serverOpts) + case nil: // backwards compatibility, allow nil value for the argument + continue + default: + panic(fmt.Sprintf("Invalid option type %T, please use a twirp.ServerOption", o)) + } + } + return serverOpts +} + // WriteError writes an HTTP response with a valid Twirp error format (code, msg, meta). // Useful outside of the Twirp server (e.g. http middleware), but does not trigger hooks. // If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err) diff --git a/internal/twirptest/no_package_name_importer/no_package_name_importer.twirp.go b/internal/twirptest/no_package_name_importer/no_package_name_importer.twirp.go index cad31265..9112cad6 100644 --- a/internal/twirptest/no_package_name_importer/no_package_name_importer.twirp.go +++ b/internal/twirptest/no_package_name_importer/no_package_name_importer.twirp.go @@ -67,9 +67,17 @@ func NewSvc2ProtobufClient(baseURL string, client HTTPClient, opts ...twirp.Clie o(&clientOpts) } + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + // Build method URLs: []/./ serviceURL := sanitizeBaseURL(baseURL) - serviceURL += baseServicePath(clientOpts.PathPrefix(), "", "Svc2") + serviceURL += baseServicePath(pathPrefix, "", "Svc2") urls := [1]string{ serviceURL + "Method", } @@ -151,9 +159,17 @@ func NewSvc2JSONClient(baseURL string, client HTTPClient, opts ...twirp.ClientOp o(&clientOpts) } + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + // Build method URLs: []/./ serviceURL := sanitizeBaseURL(baseURL) - serviceURL += baseServicePath(clientOpts.PathPrefix(), "", "Svc2") + serviceURL += baseServicePath(pathPrefix, "", "Svc2") urls := [1]string{ serviceURL + "Method", } @@ -228,26 +244,22 @@ type svc2Server struct { // HTTP requests that are routed to the right method in the provided svc implementation. // The opts are twirp.ServerOption modifiers, for example twirp.WithServerHooks(hooks). func NewSvc2Server(svc Svc2, opts ...interface{}) TwirpServer { - serverOpts := twirp.ServerOptions{} - for _, opt := range opts { - switch o := opt.(type) { - case twirp.ServerOption: - o(&serverOpts) - case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument - twirp.WithServerHooks(o)(&serverOpts) - case nil: // backwards compatibility, allow nil value for the argument - continue - default: - panic(fmt.Sprintf("Invalid option type %T on NewSvc2Server", o)) - } + serverOpts := newServerOpts(opts) + + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + jsonSkipDefaults := false + _ = serverOpts.ReadOpt("jsonSkipDefaults", &jsonSkipDefaults) + var pathPrefix string + if ok := serverOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix } return &svc2Server{ Svc2: svc, - pathPrefix: serverOpts.PathPrefix(), - interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), hooks: serverOpts.Hooks, - jsonSkipDefaults: serverOpts.JSONSkipDefaults, + interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), + pathPrefix: pathPrefix, + jsonSkipDefaults: jsonSkipDefaults, } } @@ -270,9 +282,9 @@ func (s *svc2Server) handleRequestBodyError(ctx context.Context, resp http.Respo s.writeError(ctx, resp, twirp.WrapError(malformedRequestError(msg), err)) } -// Svc2PathPrefix is a convenience constant that could used to identify URL paths. +// Svc2PathPrefix is a convenience constant that may identify URL paths. // Should be used with caution, it only matches routes generated by Twirp Go clients, -// that add a "/twirp" prefix by default, and use CamelCase service and method names. +// with the default "/twirp" prefix and default CamelCase service and method names. // More info: https://twitchtv.github.io/twirp/docs/routing.html const Svc2PathPrefix = "/twirp/Svc2/" @@ -558,6 +570,23 @@ type TwirpServer interface { PathPrefix() string } +func newServerOpts(opts []interface{}) *twirp.ServerOptions { + serverOpts := &twirp.ServerOptions{} + for _, opt := range opts { + switch o := opt.(type) { + case twirp.ServerOption: + o(serverOpts) + case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument + twirp.WithServerHooks(o)(serverOpts) + case nil: // backwards compatibility, allow nil value for the argument + continue + default: + panic(fmt.Sprintf("Invalid option type %T, please use a twirp.ServerOption", o)) + } + } + return serverOpts +} + // WriteError writes an HTTP response with a valid Twirp error format (code, msg, meta). // Useful outside of the Twirp server (e.g. http middleware), but does not trigger hooks. // If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err) diff --git a/internal/twirptest/service.twirp.go b/internal/twirptest/service.twirp.go index ca0e0145..363c829a 100644 --- a/internal/twirptest/service.twirp.go +++ b/internal/twirptest/service.twirp.go @@ -67,9 +67,17 @@ func NewHaberdasherProtobufClient(baseURL string, client HTTPClient, opts ...twi o(&clientOpts) } + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + // Build method URLs: []/./ serviceURL := sanitizeBaseURL(baseURL) - serviceURL += baseServicePath(clientOpts.PathPrefix(), "twirp.internal.twirptest", "Haberdasher") + serviceURL += baseServicePath(pathPrefix, "twirp.internal.twirptest", "Haberdasher") urls := [1]string{ serviceURL + "MakeHat", } @@ -151,9 +159,17 @@ func NewHaberdasherJSONClient(baseURL string, client HTTPClient, opts ...twirp.C o(&clientOpts) } + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + // Build method URLs: []/./ serviceURL := sanitizeBaseURL(baseURL) - serviceURL += baseServicePath(clientOpts.PathPrefix(), "twirp.internal.twirptest", "Haberdasher") + serviceURL += baseServicePath(pathPrefix, "twirp.internal.twirptest", "Haberdasher") urls := [1]string{ serviceURL + "MakeHat", } @@ -228,26 +244,22 @@ type haberdasherServer struct { // HTTP requests that are routed to the right method in the provided svc implementation. // The opts are twirp.ServerOption modifiers, for example twirp.WithServerHooks(hooks). func NewHaberdasherServer(svc Haberdasher, opts ...interface{}) TwirpServer { - serverOpts := twirp.ServerOptions{} - for _, opt := range opts { - switch o := opt.(type) { - case twirp.ServerOption: - o(&serverOpts) - case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument - twirp.WithServerHooks(o)(&serverOpts) - case nil: // backwards compatibility, allow nil value for the argument - continue - default: - panic(fmt.Sprintf("Invalid option type %T on NewHaberdasherServer", o)) - } + serverOpts := newServerOpts(opts) + + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + jsonSkipDefaults := false + _ = serverOpts.ReadOpt("jsonSkipDefaults", &jsonSkipDefaults) + var pathPrefix string + if ok := serverOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix } return &haberdasherServer{ Haberdasher: svc, - pathPrefix: serverOpts.PathPrefix(), - interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), hooks: serverOpts.Hooks, - jsonSkipDefaults: serverOpts.JSONSkipDefaults, + interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), + pathPrefix: pathPrefix, + jsonSkipDefaults: jsonSkipDefaults, } } @@ -270,9 +282,9 @@ func (s *haberdasherServer) handleRequestBodyError(ctx context.Context, resp htt s.writeError(ctx, resp, twirp.WrapError(malformedRequestError(msg), err)) } -// HaberdasherPathPrefix is a convenience constant that could used to identify URL paths. +// HaberdasherPathPrefix is a convenience constant that may identify URL paths. // Should be used with caution, it only matches routes generated by Twirp Go clients, -// that add a "/twirp" prefix by default, and use CamelCase service and method names. +// with the default "/twirp" prefix and default CamelCase service and method names. // More info: https://twitchtv.github.io/twirp/docs/routing.html const HaberdasherPathPrefix = "/twirp/twirp.internal.twirptest.Haberdasher/" @@ -558,6 +570,23 @@ type TwirpServer interface { PathPrefix() string } +func newServerOpts(opts []interface{}) *twirp.ServerOptions { + serverOpts := &twirp.ServerOptions{} + for _, opt := range opts { + switch o := opt.(type) { + case twirp.ServerOption: + o(serverOpts) + case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument + twirp.WithServerHooks(o)(serverOpts) + case nil: // backwards compatibility, allow nil value for the argument + continue + default: + panic(fmt.Sprintf("Invalid option type %T, please use a twirp.ServerOption", o)) + } + } + return serverOpts +} + // WriteError writes an HTTP response with a valid Twirp error format (code, msg, meta). // Useful outside of the Twirp server (e.g. http middleware), but does not trigger hooks. // If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err) diff --git a/internal/twirptest/service_method_same_name/service_method_same_name.twirp.go b/internal/twirptest/service_method_same_name/service_method_same_name.twirp.go index 4c7cfd51..74d92cb2 100644 --- a/internal/twirptest/service_method_same_name/service_method_same_name.twirp.go +++ b/internal/twirptest/service_method_same_name/service_method_same_name.twirp.go @@ -65,9 +65,17 @@ func NewEchoProtobufClient(baseURL string, client HTTPClient, opts ...twirp.Clie o(&clientOpts) } + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + // Build method URLs: []/./ serviceURL := sanitizeBaseURL(baseURL) - serviceURL += baseServicePath(clientOpts.PathPrefix(), "", "Echo") + serviceURL += baseServicePath(pathPrefix, "", "Echo") urls := [1]string{ serviceURL + "Echo", } @@ -149,9 +157,17 @@ func NewEchoJSONClient(baseURL string, client HTTPClient, opts ...twirp.ClientOp o(&clientOpts) } + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + // Build method URLs: []/./ serviceURL := sanitizeBaseURL(baseURL) - serviceURL += baseServicePath(clientOpts.PathPrefix(), "", "Echo") + serviceURL += baseServicePath(pathPrefix, "", "Echo") urls := [1]string{ serviceURL + "Echo", } @@ -226,26 +242,22 @@ type echoServer struct { // HTTP requests that are routed to the right method in the provided svc implementation. // The opts are twirp.ServerOption modifiers, for example twirp.WithServerHooks(hooks). func NewEchoServer(svc Echo, opts ...interface{}) TwirpServer { - serverOpts := twirp.ServerOptions{} - for _, opt := range opts { - switch o := opt.(type) { - case twirp.ServerOption: - o(&serverOpts) - case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument - twirp.WithServerHooks(o)(&serverOpts) - case nil: // backwards compatibility, allow nil value for the argument - continue - default: - panic(fmt.Sprintf("Invalid option type %T on NewEchoServer", o)) - } + serverOpts := newServerOpts(opts) + + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + jsonSkipDefaults := false + _ = serverOpts.ReadOpt("jsonSkipDefaults", &jsonSkipDefaults) + var pathPrefix string + if ok := serverOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix } return &echoServer{ Echo: svc, - pathPrefix: serverOpts.PathPrefix(), - interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), hooks: serverOpts.Hooks, - jsonSkipDefaults: serverOpts.JSONSkipDefaults, + interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), + pathPrefix: pathPrefix, + jsonSkipDefaults: jsonSkipDefaults, } } @@ -268,9 +280,9 @@ func (s *echoServer) handleRequestBodyError(ctx context.Context, resp http.Respo s.writeError(ctx, resp, twirp.WrapError(malformedRequestError(msg), err)) } -// EchoPathPrefix is a convenience constant that could used to identify URL paths. +// EchoPathPrefix is a convenience constant that may identify URL paths. // Should be used with caution, it only matches routes generated by Twirp Go clients, -// that add a "/twirp" prefix by default, and use CamelCase service and method names. +// with the default "/twirp" prefix and default CamelCase service and method names. // More info: https://twitchtv.github.io/twirp/docs/routing.html const EchoPathPrefix = "/twirp/Echo/" @@ -556,6 +568,23 @@ type TwirpServer interface { PathPrefix() string } +func newServerOpts(opts []interface{}) *twirp.ServerOptions { + serverOpts := &twirp.ServerOptions{} + for _, opt := range opts { + switch o := opt.(type) { + case twirp.ServerOption: + o(serverOpts) + case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument + twirp.WithServerHooks(o)(serverOpts) + case nil: // backwards compatibility, allow nil value for the argument + continue + default: + panic(fmt.Sprintf("Invalid option type %T, please use a twirp.ServerOption", o)) + } + } + return serverOpts +} + // WriteError writes an HTTP response with a valid Twirp error format (code, msg, meta). // Useful outside of the Twirp server (e.g. http middleware), but does not trigger hooks. // If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err) diff --git a/internal/twirptest/snake_case_names/snake_case_names.twirp.go b/internal/twirptest/snake_case_names/snake_case_names.twirp.go index 5e099f1d..0948a537 100644 --- a/internal/twirptest/snake_case_names/snake_case_names.twirp.go +++ b/internal/twirptest/snake_case_names/snake_case_names.twirp.go @@ -70,17 +70,25 @@ func NewHaberdasherV1ProtobufClient(baseURL string, client HTTPClient, opts ...t o(&clientOpts) } + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + // Build method URLs: []/./ serviceURL := sanitizeBaseURL(baseURL) - if clientOpts.LiteralURLs { - serviceURL += baseServicePath(clientOpts.PathPrefix(), "twirp.internal.twirptest.snake_case_names", "Haberdasher_v1") + if literalURLs { + serviceURL += baseServicePath(pathPrefix, "twirp.internal.twirptest.snake_case_names", "Haberdasher_v1") } else { - serviceURL += baseServicePath(clientOpts.PathPrefix(), "twirp.internal.twirptest.snake_case_names", "HaberdasherV1") + serviceURL += baseServicePath(pathPrefix, "twirp.internal.twirptest.snake_case_names", "HaberdasherV1") } urls := [1]string{ serviceURL + "MakeHatV1", } - if clientOpts.LiteralURLs { + if literalURLs { urls = [1]string{ serviceURL + "MakeHat_v1", } @@ -163,17 +171,25 @@ func NewHaberdasherV1JSONClient(baseURL string, client HTTPClient, opts ...twirp o(&clientOpts) } + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + // Build method URLs: []/./ serviceURL := sanitizeBaseURL(baseURL) - if clientOpts.LiteralURLs { - serviceURL += baseServicePath(clientOpts.PathPrefix(), "twirp.internal.twirptest.snake_case_names", "Haberdasher_v1") + if literalURLs { + serviceURL += baseServicePath(pathPrefix, "twirp.internal.twirptest.snake_case_names", "Haberdasher_v1") } else { - serviceURL += baseServicePath(clientOpts.PathPrefix(), "twirp.internal.twirptest.snake_case_names", "HaberdasherV1") + serviceURL += baseServicePath(pathPrefix, "twirp.internal.twirptest.snake_case_names", "HaberdasherV1") } urls := [1]string{ serviceURL + "MakeHatV1", } - if clientOpts.LiteralURLs { + if literalURLs { urls = [1]string{ serviceURL + "MakeHat_v1", } @@ -249,26 +265,22 @@ type haberdasherV1Server struct { // HTTP requests that are routed to the right method in the provided svc implementation. // The opts are twirp.ServerOption modifiers, for example twirp.WithServerHooks(hooks). func NewHaberdasherV1Server(svc HaberdasherV1, opts ...interface{}) TwirpServer { - serverOpts := twirp.ServerOptions{} - for _, opt := range opts { - switch o := opt.(type) { - case twirp.ServerOption: - o(&serverOpts) - case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument - twirp.WithServerHooks(o)(&serverOpts) - case nil: // backwards compatibility, allow nil value for the argument - continue - default: - panic(fmt.Sprintf("Invalid option type %T on NewHaberdasherV1Server", o)) - } + serverOpts := newServerOpts(opts) + + // Using ReadOpt allows backwards and forwads compatibility with new options in the future + jsonSkipDefaults := false + _ = serverOpts.ReadOpt("jsonSkipDefaults", &jsonSkipDefaults) + var pathPrefix string + if ok := serverOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix } return &haberdasherV1Server{ HaberdasherV1: svc, - pathPrefix: serverOpts.PathPrefix(), - interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), hooks: serverOpts.Hooks, - jsonSkipDefaults: serverOpts.JSONSkipDefaults, + interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), + pathPrefix: pathPrefix, + jsonSkipDefaults: jsonSkipDefaults, } } @@ -291,9 +303,9 @@ func (s *haberdasherV1Server) handleRequestBodyError(ctx context.Context, resp h s.writeError(ctx, resp, twirp.WrapError(malformedRequestError(msg), err)) } -// HaberdasherV1PathPrefix is a convenience constant that could used to identify URL paths. +// HaberdasherV1PathPrefix is a convenience constant that may identify URL paths. // Should be used with caution, it only matches routes generated by Twirp Go clients, -// that add a "/twirp" prefix by default, and use CamelCase service and method names. +// with the default "/twirp" prefix and default CamelCase service and method names. // More info: https://twitchtv.github.io/twirp/docs/routing.html const HaberdasherV1PathPrefix = "/twirp/twirp.internal.twirptest.snake_case_names.HaberdasherV1/" @@ -579,6 +591,23 @@ type TwirpServer interface { PathPrefix() string } +func newServerOpts(opts []interface{}) *twirp.ServerOptions { + serverOpts := &twirp.ServerOptions{} + for _, opt := range opts { + switch o := opt.(type) { + case twirp.ServerOption: + o(serverOpts) + case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument + twirp.WithServerHooks(o)(serverOpts) + case nil: // backwards compatibility, allow nil value for the argument + continue + default: + panic(fmt.Sprintf("Invalid option type %T, please use a twirp.ServerOption", o)) + } + } + return serverOpts +} + // WriteError writes an HTTP response with a valid Twirp error format (code, msg, meta). // Useful outside of the Twirp server (e.g. http middleware), but does not trigger hooks. // If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err) diff --git a/protoc-gen-twirp/generator.go b/protoc-gen-twirp/generator.go index aeccd67f..2acade7f 100644 --- a/protoc-gen-twirp/generator.go +++ b/protoc-gen-twirp/generator.go @@ -362,9 +362,11 @@ func (t *twirp) generateUtilImports() { } // Generate utility functions used in Twirp code. -// These should be generated just once per package. +// These functions are generated only once per package; when generating for +// multiple services they are declared only once. func (t *twirp) generateUtils() { t.sectionComment(`Utils`) + t.P(`// HTTPClient is the interface used by generated clients to send HTTP requests.`) t.P(`// It is fulfilled by *(net/http).Client, which is sufficient for most users.`) t.P(`// Users can provide their own implementation for special retry policies.`) @@ -377,6 +379,7 @@ func (t *twirp) generateUtils() { t.P(` Do(req *`, t.pkgs["http"], `.Request) (*`, t.pkgs["http"], `.Response, error)`) t.P(`}`) t.P() + t.P(`// TwirpServer is the interface generated server structs will support: they're`) t.P(`// HTTP handlers with additional methods for accessing metadata about the`) t.P(`// service. Those accessors are a low-level API for building reflection tools.`) @@ -384,6 +387,7 @@ func (t *twirp) generateUtils() { t.P(`type TwirpServer interface {`) t.P(` `, t.pkgs["http"], `.Handler`) t.P() + t.P(` // ServiceDescriptor returns gzipped bytes describing the .proto file that`) t.P(` // this service was generated from. Once unzipped, the bytes can be`) t.P(` // unmarshalled as a`) @@ -406,6 +410,24 @@ func (t *twirp) generateUtils() { t.P(`}`) t.P() + t.P(`func newServerOpts(opts []interface{}) *`, t.pkgs["twirp"], `.ServerOptions {`) + t.P(` serverOpts := &`, t.pkgs["twirp"], `.ServerOptions{}`) + t.P(` for _, opt := range opts {`) + t.P(` switch o := opt.(type) {`) + t.P(` case `, t.pkgs["twirp"], `.ServerOption:`) + t.P(` o(serverOpts)`) + t.P(` case *`, t.pkgs["twirp"], `.ServerHooks: // backwards compatibility, allow to specify hooks as an argument`) + t.P(` twirp.WithServerHooks(o)(serverOpts)`) + t.P(` case nil: // backwards compatibility, allow nil value for the argument`) + t.P(` continue`) + t.P(` default:`) + t.P(` panic(`, t.pkgs["fmt"], `.Sprintf("Invalid option type %T, please use a twirp.ServerOption", o))`) + t.P(` }`) + t.P(` }`) + t.P(` return serverOpts`) + t.P(`}`) + t.P() + t.P(`// WriteError writes an HTTP response with a valid Twirp error format (code, msg, meta).`) t.P(`// Useful outside of the Twirp server (e.g. http middleware), but does not trigger hooks.`) t.P(`// If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err)`) @@ -1000,17 +1022,27 @@ func (t *twirp) generateClient(name string, file *descriptor.FileDescriptorProto t.P(` for _, o := range opts {`) t.P(` o(&clientOpts)`) t.P(` }`) + t.P() + + t.P(` // Using ReadOpt allows backwards and forwads compatibility with new options in the future`) + t.P(` literalURLs := false`) + t.P(` _ = clientOpts.ReadOpt("literalURLs", &literalURLs)`) + t.P(` var pathPrefix string`) + t.P(` if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok {`) + t.P(` pathPrefix = "/twirp" // default prefix`) + t.P(` }`) + t.P() if len(service.Method) > 0 { t.P(` // Build method URLs: []/./`) t.P(` serviceURL := sanitizeBaseURL(baseURL)`) if servNameLit == servNameCc { - t.P(` serviceURL += baseServicePath(clientOpts.PathPrefix(), "`, servPkg, `", "`, servNameCc, `")`) + t.P(` serviceURL += baseServicePath(pathPrefix, "`, servPkg, `", "`, servNameCc, `")`) } else { // proto service name is not CamelCased, then it needs to check client option to decide if needs to change case - t.P(` if clientOpts.LiteralURLs {`) - t.P(` serviceURL += baseServicePath(clientOpts.PathPrefix(), "`, servPkg, `", "`, servNameLit, `")`) + t.P(` if literalURLs {`) + t.P(` serviceURL += baseServicePath(pathPrefix, "`, servPkg, `", "`, servNameLit, `")`) t.P(` } else {`) - t.P(` serviceURL += baseServicePath(clientOpts.PathPrefix(), "`, servPkg, `", "`, servNameCc, `")`) + t.P(` serviceURL += baseServicePath(pathPrefix, "`, servPkg, `", "`, servNameCc, `")`) t.P(` }`) } } @@ -1030,7 +1062,7 @@ func (t *twirp) generateClient(name string, file *descriptor.FileDescriptorProto } } if !allMethodsCamelCased { - t.P(` if clientOpts.LiteralURLs {`) + t.P(` if literalURLs {`) t.P(` urls = [`, methCnt, `]string{`) for _, method := range service.Method { t.P(` serviceURL + "`, methodNameLiteral(method), `",`) @@ -1127,26 +1159,22 @@ func (t *twirp) generateServer(file *descriptor.FileDescriptorProto, service *de t.P(`// HTTP requests that are routed to the right method in the provided svc implementation.`) t.P(`// The opts are twirp.ServerOption modifiers, for example twirp.WithServerHooks(hooks).`) t.P(`func New`, servName, `Server(svc `, servName, `, opts ...interface{}) TwirpServer {`) - t.P(` serverOpts := `, t.pkgs["twirp"], `.ServerOptions{}`) - t.P(` for _, opt := range opts {`) - t.P(` switch o := opt.(type) {`) - t.P(` case `, t.pkgs["twirp"], `.ServerOption:`) - t.P(` o(&serverOpts)`) - t.P(` case *`, t.pkgs["twirp"], `.ServerHooks: // backwards compatibility, allow to specify hooks as an argument`) - t.P(` twirp.WithServerHooks(o)(&serverOpts)`) - t.P(` case nil: // backwards compatibility, allow nil value for the argument`) - t.P(` continue`) - t.P(` default:`) - t.P(` panic(`, t.pkgs["fmt"], `.Sprintf("Invalid option type %T on New`, servName, `Server", o))`) - t.P(` }`) + t.P(` serverOpts := newServerOpts(opts)`) + t.P() + t.P(` // Using ReadOpt allows backwards and forwads compatibility with new options in the future`) + t.P(` jsonSkipDefaults := false`) + t.P(` _ = serverOpts.ReadOpt("jsonSkipDefaults", &jsonSkipDefaults)`) + t.P(` var pathPrefix string`) + t.P(` if ok := serverOpts.ReadOpt("pathPrefix", &pathPrefix); !ok {`) + t.P(` pathPrefix = "/twirp" // default prefix`) t.P(` }`) t.P() t.P(` return &`, servStruct, `{`) t.P(` `, servName, `: svc,`) - t.P(` pathPrefix: serverOpts.PathPrefix(),`) - t.P(` interceptor: `, t.pkgs["twirp"], `.ChainInterceptors(serverOpts.Interceptors...),`) t.P(` hooks: serverOpts.Hooks,`) - t.P(` jsonSkipDefaults: serverOpts.JSONSkipDefaults,`) + t.P(` interceptor: `, t.pkgs["twirp"], `.ChainInterceptors(serverOpts.Interceptors...),`) + t.P(` pathPrefix: pathPrefix,`) + t.P(` jsonSkipDefaults: jsonSkipDefaults,`) t.P(` }`) t.P(`}`) t.P() @@ -1191,9 +1219,9 @@ func (t *twirp) generateServerRouting(servStruct string, file *descriptor.FileDe pkgServNameLit := pkgServiceNameLiteral(file, service) pkgServNameCc := pkgServiceNameCamelCased(file, service) - t.P(`// `, servName, `PathPrefix is a convenience constant that could used to identify URL paths.`) + t.P(`// `, servName, `PathPrefix is a convenience constant that may identify URL paths.`) t.P(`// Should be used with caution, it only matches routes generated by Twirp Go clients,`) - t.P(`// that add a "/twirp" prefix by default, and use CamelCase service and method names.`) + t.P(`// with the default "/twirp" prefix and default CamelCase service and method names.`) t.P(`// More info: https://twitchtv.github.io/twirp/docs/routing.html`) t.P(`const `, servName, `PathPrefix = "/twirp/`, pkgServNameCc, `/"`) t.P() diff --git a/server_options.go b/server_options.go index e26ee485..7ec3f7bd 100644 --- a/server_options.go +++ b/server_options.go @@ -14,37 +14,50 @@ package twirp import ( "context" + "reflect" ) -// ServerOption is a functional option for extending a Twirp client. +// ServerOption is a functional option for extending a Twirp service. type ServerOption func(*ServerOptions) -// ServerOptions encapsulate the configurable parameters on a Twirp client. -type ServerOptions struct { - Interceptors []Interceptor - Hooks *ServerHooks - pathPrefix *string - JSONSkipDefaults bool -} - -func (opts *ServerOptions) PathPrefix() string { - if opts.pathPrefix == nil { - return "/twirp" // default prefix +// WithServerHooks defines the hooks for a Twirp server. +func WithServerHooks(hooks *ServerHooks) ServerOption { + return func(opts *ServerOptions) { + opts.Hooks = hooks } - return *opts.pathPrefix } // WithServerInterceptors defines the interceptors for a Twirp server. func WithServerInterceptors(interceptors ...Interceptor) ServerOption { - return func(o *ServerOptions) { - o.Interceptors = append(o.Interceptors, interceptors...) + return func(opts *ServerOptions) { + opts.Interceptors = append(opts.Interceptors, interceptors...) } } -// WithServerHooks defines the hooks for a Twirp server. -func WithServerHooks(hooks *ServerHooks) ServerOption { - return func(o *ServerOptions) { - o.Hooks = hooks +// WithServerPathPrefix specifies a different prefix for routing. +// If not specified, the "/twirp" prefix is used by default. +// An empty value "" can be speficied to use no prefix. +// The clients must be configured to send requests using the same prefix. +// URL format: "[]/./" +// More info on Twirp docs: https://twitchtv.github.io/twirp/docs/routing.html +func WithServerPathPrefix(prefix string) ServerOption { + return func(opts *ServerOptions) { + opts.setOpt("pathPrefix", prefix) + opts.pathPrefix = &prefix // for code generated before v8.1.0 + } +} + +// WithServerJSONSkipDefaults configures JSON serialization to skip +// unpopulated or default values in JSON responses, which results in +// smaller response sizes. This was the default before v7 and can be +// enabled for full backwards compatibility if required. +// This is now disabled by default, because JSON serialization is +// commonly used for manual debugging, in which case it is useful +// to see the full shape of the response. +func WithServerJSONSkipDefaults(skipDefaults bool) ServerOption { + return func(opts *ServerOptions) { + opts.setOpt("jsonSkipDefaults", skipDefaults) + opts.JSONSkipDefaults = skipDefaults // for code generated before v8.1.0 } } @@ -152,27 +165,63 @@ func ChainHooks(hooks ...*ServerHooks) *ServerHooks { } } -// WithServerPathPrefix specifies a different prefix for routing. -// If not specified, the "/twirp" prefix is used by default. -// An empty value "" can be speficied to use no prefix. -// The clients must be configured to send requests using the same prefix. -// URL format: "[]/./" -// More info on Twirp docs: https://twitchtv.github.io/twirp/docs/routing.html -func WithServerPathPrefix(prefix string) ServerOption { - return func(o *ServerOptions) { - o.pathPrefix = &prefix +// ServerOptions encapsulate the configurable parameters on a Twirp server. +// This type is meant to be used only by generated code. +type ServerOptions struct { + // Untyped options map. The methods setOpt and ReadOpt are used to set + // and read options. The options are untyped so when a new option is added, + // newly generated code can still work with older versions of the runtime. + m map[string]interface{} + + Hooks *ServerHooks + Interceptors []Interceptor + + // Properties below are only used by code that was + // generated by older versions of Twirp (before v8.1.0). + // New options with standard types added in the future + // don't need new properties, they should use ReadOpt. + JSONSkipDefaults bool + pathPrefix *string +} + +// ReadOpt extracts an option to a pointer value, +// returns true if the option exists and was extracted. +// This method is meant to be used by generated code, +// keeping the type dependency outside of the runtime. +// +// Usage example: +// +// opts.setOpt("fooOpt", 123) +// var foo int +// ok := opts.ReadOpt("fooOpt", &int) +// +func (opts *ServerOptions) ReadOpt(key string, out interface{}) bool { + val, ok := opts.m[key] + if !ok { + return false + } + + rout := reflect.ValueOf(out) + if rout.Kind() != reflect.Ptr { + panic("ReadOpt(key, out); out must be a pointer but it was not") } + rout.Elem().Set(reflect.ValueOf(val)) + return true } -// WithServerJSONSkipDefaults configures JSON serialization to skip -// unpopulated or default values in JSON responses, which results in -// smaller response sizes. This was the default before v7 and can be -// enabled for full backwards compatibility if required. -// This is now disabled by default, because JSON serialization is -// commonly used for manual debugging, in which case it is useful -// to see the full shape of the response. -func WithServerJSONSkipDefaults(skipDefaults bool) ServerOption { - return func(o *ServerOptions) { - o.JSONSkipDefaults = skipDefaults +// setOpt adds an option key/value. It is used by ServerOption helpers. +// The value can be extracted with ReadOpt by passing a pointer to the same type. +func (opts *ServerOptions) setOpt(key string, val interface{}) { + if opts.m == nil { + opts.m = make(map[string]interface{}) + } + opts.m[key] = val +} + +// PathPrefix() is used only by clients generated before v8.1.0 +func (opts *ServerOptions) PathPrefix() string { + if opts.pathPrefix == nil { + return "/twirp" // default prefix } + return *opts.pathPrefix } diff --git a/server_options_test.go b/server_options_test.go index f02e76e9..5cdef765 100644 --- a/server_options_test.go +++ b/server_options_test.go @@ -19,6 +19,41 @@ import ( "testing" ) +func TestServerOptionsReadOpt(t *testing.T) { + opts := &ServerOptions{} + ok := false + + var fooNum int + ok = opts.ReadOpt("fooNum", &fooNum) + if ok { + t.Errorf("option 'fooNum' does not exist, opts.ReadOpt should have returned false") + } + + opts.setOpt("fooNum", 455) + ok = opts.ReadOpt("fooNum", &fooNum) + if !ok || fooNum != 455 { + t.Errorf("option 'fooNum' expected to be 455") + } + + var jsonSkipDefaults bool + ok = opts.ReadOpt("jsonSkipDefaults", &jsonSkipDefaults) + if ok { + t.Errorf("option 'jsonSkipDefaults' does not exist, opts.ReadOpt should have returned false") + } + + WithServerJSONSkipDefaults(true)(opts) + ok = opts.ReadOpt("jsonSkipDefaults", &jsonSkipDefaults) + if !ok || !jsonSkipDefaults { + t.Errorf("option 'jsonSkipDefaults' expected to be true, ok: %v, val: %v", ok, jsonSkipDefaults) + } + + WithServerJSONSkipDefaults(false)(opts) + ok = opts.ReadOpt("jsonSkipDefaults", &jsonSkipDefaults) + if !ok || jsonSkipDefaults { + t.Errorf("option 'jsonSkipDefaults' expected to be false, ok: %v, val: %v", ok, jsonSkipDefaults) + } +} + func TestChainHooks(t *testing.T) { var ( hook1 = new(ServerHooks)