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

runtimevar: add infrastructure for opening Variables via URL #1413

Merged
merged 2 commits into from
Feb 28, 2019
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
2 changes: 1 addition & 1 deletion blob/blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -867,7 +867,7 @@ type WriterOptions struct {
BeforeWrite func(asFunc func(interface{}) bool) error
}

// A type that implements BucketURLOpener can open buckets based on a URL.
// BucketURLOpener represents types that can open buckets based on a URL.
vangent marked this conversation as resolved.
Show resolved Hide resolved
// The opener must not modify the URL argument. OpenBucketURL must be safe to
// call from multiple goroutines.
//
Expand Down
76 changes: 76 additions & 0 deletions runtimevar/runtimevar.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ import (
"context"
"encoding/gob"
"encoding/json"
"fmt"
"net/url"
"reflect"
"time"

Expand Down Expand Up @@ -216,6 +218,80 @@ func (c *Variable) ErrorAs(err error, i interface{}) bool {
return gcerr.ErrorAs(err, i, c.watcher.ErrorAs)
}

// VariableURLOpener represents types than can open Variables based on a URL.
// The opener must not modify the URL argument. OpenVariableURL must be safe to
// call from multiple goroutines.
//
// This interface is generally implemented by types in driver packages.
type VariableURLOpener interface {
OpenVariableURL(ctx context.Context, u *url.URL) (*Variable, error)
}

// URLMux is a URL opener multiplexer. It matches the scheme of the URLs
// against a set of registered schemes and calls the opener that matches the
// URL's scheme.
//
// The zero value is a multiplexer with no registered schemes.
type URLMux struct {
schemes map[string]VariableURLOpener
}

// RegisterVariable registers the opener with the given scheme. If an opener
// already exists for the scheme, RegisterVariable panics.
func (mux *URLMux) RegisterVariable(scheme string, opener VariableURLOpener) {
if mux.schemes == nil {
mux.schemes = make(map[string]VariableURLOpener)
} else if _, exists := mux.schemes[scheme]; exists {
panic(fmt.Errorf("scheme %q already registered on mux", scheme))
}
mux.schemes[scheme] = opener
}

// OpenVariable calls OpenVariableURL with the URL parsed from urlstr.
// OpenVariable is safe to call from multiple goroutines.
func (mux *URLMux) OpenVariable(ctx context.Context, urlstr string) (*Variable, error) {
u, err := url.Parse(urlstr)
if err != nil {
return nil, fmt.Errorf("open variable: %v", err)
}
return mux.OpenVariableURL(ctx, u)
}

// OpenVariableURL dispatches the URL to the opener that is registered with the
// URL's scheme. OpenVariableURL is safe to call from multiple goroutines.
func (mux *URLMux) OpenVariableURL(ctx context.Context, u *url.URL) (*Variable, error) {
if u.Scheme == "" {
return nil, fmt.Errorf("open variable %q: no scheme in URL", u)
}
var opener VariableURLOpener
if mux != nil {
opener = mux.schemes[u.Scheme]
}
if opener == nil {
return nil, fmt.Errorf("open variable %q: no provider registered for %s", u, u.Scheme)
}
return opener.OpenVariableURL(ctx, u)
}

var defaultURLMux = new(URLMux)

// DefaultURLMux returns the URLMux used by OpenVariable.
//
// Driver packages can use this to register their VariableURLOpener on the mux.
func DefaultURLMux() *URLMux {
return defaultURLMux
}

// OpenVariable opens the variable identified by the URL given. URL openers must be
// registered in the DefaultURLMux, which is typically done in driver
// packages' initialization.
//
// See the URLOpener documentation in provider-specific subpackages for more
// details on supported scheme(s) and URL parameter(s).
func OpenVariable(ctx context.Context, urlstr string) (*Variable, error) {
return defaultURLMux.OpenVariable(ctx, urlstr)
}

// Decode is a function type for unmarshaling/decoding a slice of bytes into
// an arbitrary type. Decode functions are used when creating a Decoder via
// NewDecoder. This package provides common Decode functions including
Expand Down
97 changes: 97 additions & 0 deletions runtimevar/runtimevar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ import (
"encoding/json"
"errors"
"fmt"
"net/url"
"reflect"
"sync"
"testing"
"time"

Expand Down Expand Up @@ -247,6 +249,101 @@ func TestErrorsAreWrapped(t *testing.T) {
verifyWrap("Close", err)
}

var (
testOpenOnce sync.Once
testOpenGot *url.URL
)

func TestURLMux(t *testing.T) {
ctx := context.Background()
var got *url.URL

mux := new(URLMux)
// Register scheme foo to always return nil. Sets got as a side effect
mux.RegisterVariable("foo", variableURLOpenFunc(func(_ context.Context, u *url.URL) (*Variable, error) {
got = u
return nil, nil
}))
// Register scheme err to always return an error.
mux.RegisterVariable("err", variableURLOpenFunc(func(_ context.Context, u *url.URL) (*Variable, error) {
return nil, errors.New("fail")
}))

for _, tc := range []struct {
name string
url string
wantErr bool
}{
{
name: "empty URL",
wantErr: true,
},
{
name: "invalid URL",
url: ":foo",
wantErr: true,
},
{
name: "invalid URL no scheme",
url: "foo",
wantErr: true,
},
{
name: "unregistered scheme",
url: "bar://myvar",
wantErr: true,
},
{
name: "func returns error",
url: "err://myvar",
wantErr: true,
},
{
name: "no query options",
url: "foo://myvar",
},
{
name: "empty query options",
url: "foo://myvar?",
},
{
name: "query options",
url: "foo://myvar?aAa=bBb&cCc=dDd",
},
{
name: "multiple query options",
url: "foo://myvar?x=a&x=b&x=c",
},
{
name: "fancy var name",
url: "foo:///foo/bar/baz",
},
} {
t.Run(tc.name, func(t *testing.T) {
_, gotErr := mux.OpenVariable(ctx, tc.url)
if (gotErr != nil) != tc.wantErr {
t.Fatalf("got err %v, want error %v", gotErr, tc.wantErr)
}
if gotErr != nil {
return
}
want, err := url.Parse(tc.url)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("got\n%v\nwant\n%v\ndiff\n%s", got, want, diff)
}
})
}
}

type variableURLOpenFunc func(context.Context, *url.URL) (*Variable, error)

func (f variableURLOpenFunc) OpenVariableURL(ctx context.Context, u *url.URL) (*Variable, error) {
return f(ctx, u)
}

func TestDecoder(t *testing.T) {
type Struct struct {
FieldA string
Expand Down