Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ServerOptions and ClientOptions use untyped dynamic options #319

Merged
merged 4 commits into from
Jun 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 87 additions & 38 deletions client_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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: "<baseURL>[<prefix>]/<package>.<Service>/<Method>"
// 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
}
}

Expand Down Expand Up @@ -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: "<baseURL>[<prefix>]/<package>.<Service>/<Method>"
// 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
}
35 changes: 35 additions & 0 deletions client_options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
67 changes: 48 additions & 19 deletions clientcompat/internal/clientcompat/clientcompat.twirp.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading