From 4c81c9dddc5e3996bf5a4fc7017b9d4a81e6fd46 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Mon, 8 Jun 2015 11:33:06 -0400 Subject: [PATCH 1/2] generate plugin clients via template Signed-off-by: Brian Goff --- pkg/plugins/pluginrpc-gen/fixtures/foo.go | 35 +++++ pkg/plugins/pluginrpc-gen/main.go | 91 ++++++++++++ pkg/plugins/pluginrpc-gen/parser.go | 162 +++++++++++++++++++++ pkg/plugins/pluginrpc-gen/parser_test.go | 168 ++++++++++++++++++++++ pkg/plugins/pluginrpc-gen/template.go | 97 +++++++++++++ 5 files changed, 553 insertions(+) create mode 100644 pkg/plugins/pluginrpc-gen/fixtures/foo.go create mode 100644 pkg/plugins/pluginrpc-gen/main.go create mode 100644 pkg/plugins/pluginrpc-gen/parser.go create mode 100644 pkg/plugins/pluginrpc-gen/parser_test.go create mode 100644 pkg/plugins/pluginrpc-gen/template.go diff --git a/pkg/plugins/pluginrpc-gen/fixtures/foo.go b/pkg/plugins/pluginrpc-gen/fixtures/foo.go new file mode 100644 index 0000000000..4e73fc10ef --- /dev/null +++ b/pkg/plugins/pluginrpc-gen/fixtures/foo.go @@ -0,0 +1,35 @@ +package foo + +type wobble struct { + Some string + Val string + Inception *wobble +} + +type Fooer interface{} + +type Fooer2 interface { + Foo() +} + +type Fooer3 interface { + Foo() + Bar(a string) + Baz(a string) (err error) + Qux(a, b string) (val string, err error) + Wobble() (w *wobble) + Wiggle() (w wobble) +} + +type Fooer4 interface { + Foo() error +} + +type Bar interface { + Boo(a string, b string) (s string, err error) +} + +type Fooer5 interface { + Foo() + Bar +} diff --git a/pkg/plugins/pluginrpc-gen/main.go b/pkg/plugins/pluginrpc-gen/main.go new file mode 100644 index 0000000000..2130af1240 --- /dev/null +++ b/pkg/plugins/pluginrpc-gen/main.go @@ -0,0 +1,91 @@ +package main + +import ( + "bytes" + "flag" + "fmt" + "go/format" + "io/ioutil" + "os" + "unicode" + "unicode/utf8" +) + +type stringSet struct { + values map[string]struct{} +} + +func (s stringSet) String() string { + return "" +} + +func (s stringSet) Set(value string) error { + s.values[value] = struct{}{} + return nil +} +func (s stringSet) GetValues() map[string]struct{} { + return s.values +} + +var ( + typeName = flag.String("type", "", "interface type to generate plugin rpc proxy for") + rpcName = flag.String("name", *typeName, "RPC name, set if different from type") + inputFile = flag.String("i", "", "input file path") + outputFile = flag.String("o", *inputFile+"_proxy.go", "output file path") + + skipFuncs map[string]struct{} + flSkipFuncs = stringSet{make(map[string]struct{})} + + flBuildTags = stringSet{make(map[string]struct{})} +) + +func errorOut(msg string, err error) { + if err == nil { + return + } + fmt.Fprintf(os.Stderr, "%s: %v\n", msg, err) + os.Exit(1) +} + +func checkFlags() error { + if *outputFile == "" { + return fmt.Errorf("missing required flag `-o`") + } + if *inputFile == "" { + return fmt.Errorf("missing required flag `-i`") + } + return nil +} + +func main() { + flag.Var(flSkipFuncs, "skip", "skip parsing for function") + flag.Var(flBuildTags, "tag", "build tags to add to generated files") + flag.Parse() + skipFuncs = flSkipFuncs.GetValues() + + errorOut("error", checkFlags()) + + pkg, err := Parse(*inputFile, *typeName) + errorOut(fmt.Sprintf("error parsing requested type %s", *typeName), err) + + var analysis = struct { + InterfaceType string + RPCName string + BuildTags map[string]struct{} + *parsedPkg + }{toLower(*typeName), *rpcName, flBuildTags.GetValues(), pkg} + var buf bytes.Buffer + + errorOut("parser error", generatedTempl.Execute(&buf, analysis)) + src, err := format.Source(buf.Bytes()) + errorOut("error formating generated source", err) + errorOut("error writing file", ioutil.WriteFile(*outputFile, src, 0644)) +} + +func toLower(s string) string { + if s == "" { + return "" + } + r, n := utf8.DecodeRuneInString(s) + return string(unicode.ToLower(r)) + s[n:] +} diff --git a/pkg/plugins/pluginrpc-gen/parser.go b/pkg/plugins/pluginrpc-gen/parser.go new file mode 100644 index 0000000000..b9746f878d --- /dev/null +++ b/pkg/plugins/pluginrpc-gen/parser.go @@ -0,0 +1,162 @@ +package main + +import ( + "errors" + "fmt" + "go/ast" + "go/parser" + "go/token" + "reflect" + "strings" +) + +var ErrBadReturn = errors.New("found return arg with no name: all args must be named") + +type ErrUnexpectedType struct { + expected string + actual interface{} +} + +func (e ErrUnexpectedType) Error() string { + return fmt.Sprintf("got wrong type expecting %s, got: %v", e.expected, reflect.TypeOf(e.actual)) +} + +type parsedPkg struct { + Name string + Functions []function +} + +type function struct { + Name string + Args []arg + Returns []arg + Doc string +} + +type arg struct { + Name string + ArgType string +} + +func (a *arg) String() string { + return strings.ToLower(a.Name) + " " + strings.ToLower(a.ArgType) +} + +// Parses the given file for an interface definition with the given name +func Parse(filePath string, objName string) (*parsedPkg, error) { + fs := token.NewFileSet() + pkg, err := parser.ParseFile(fs, filePath, nil, parser.AllErrors) + if err != nil { + return nil, err + } + p := &parsedPkg{} + p.Name = pkg.Name.Name + obj, exists := pkg.Scope.Objects[objName] + if !exists { + return nil, fmt.Errorf("could not find object %s in %s", objName, filePath) + } + if obj.Kind != ast.Typ { + return nil, fmt.Errorf("exected type, got %s", obj.Kind) + } + spec, ok := obj.Decl.(*ast.TypeSpec) + if !ok { + return nil, ErrUnexpectedType{"*ast.TypeSpec", obj.Decl} + } + iface, ok := spec.Type.(*ast.InterfaceType) + if !ok { + return nil, ErrUnexpectedType{"*ast.InterfaceType", spec.Type} + } + + p.Functions, err = parseInterface(iface) + if err != nil { + return nil, err + } + + return p, nil +} + +func parseInterface(iface *ast.InterfaceType) ([]function, error) { + var functions []function + for _, field := range iface.Methods.List { + switch f := field.Type.(type) { + case *ast.FuncType: + method, err := parseFunc(field) + if err != nil { + return nil, err + } + if method == nil { + continue + } + functions = append(functions, *method) + case *ast.Ident: + spec, ok := f.Obj.Decl.(*ast.TypeSpec) + if !ok { + return nil, ErrUnexpectedType{"*ast.TypeSpec", f.Obj.Decl} + } + iface, ok := spec.Type.(*ast.InterfaceType) + if !ok { + return nil, ErrUnexpectedType{"*ast.TypeSpec", spec.Type} + } + funcs, err := parseInterface(iface) + if err != nil { + fmt.Println(err) + continue + } + functions = append(functions, funcs...) + default: + return nil, ErrUnexpectedType{"*astFuncType or *ast.Ident", f} + } + } + return functions, nil +} + +func parseFunc(field *ast.Field) (*function, error) { + f := field.Type.(*ast.FuncType) + method := &function{Name: field.Names[0].Name} + if _, exists := skipFuncs[method.Name]; exists { + fmt.Println("skipping:", method.Name) + return nil, nil + } + if f.Params != nil { + args, err := parseArgs(f.Params.List) + if err != nil { + return nil, err + } + method.Args = args + } + if f.Results != nil { + returns, err := parseArgs(f.Results.List) + if err != nil { + return nil, fmt.Errorf("error parsing function returns for %q: %v", method.Name, err) + } + method.Returns = returns + } + return method, nil +} + +func parseArgs(fields []*ast.Field) ([]arg, error) { + var args []arg + for _, f := range fields { + if len(f.Names) == 0 { + return nil, ErrBadReturn + } + for _, name := range f.Names { + var typeName string + switch argType := f.Type.(type) { + case *ast.Ident: + typeName = argType.Name + case *ast.StarExpr: + i, ok := argType.X.(*ast.Ident) + if !ok { + return nil, ErrUnexpectedType{"*ast.Ident", f.Type} + } + typeName = "*" + i.Name + default: + return nil, ErrUnexpectedType{"*ast.Ident or *ast.StarExpr", f.Type} + } + + args = append(args, arg{name.Name, typeName}) + } + } + return args, nil +} diff --git a/pkg/plugins/pluginrpc-gen/parser_test.go b/pkg/plugins/pluginrpc-gen/parser_test.go new file mode 100644 index 0000000000..6c5665fe2e --- /dev/null +++ b/pkg/plugins/pluginrpc-gen/parser_test.go @@ -0,0 +1,168 @@ +package main + +import ( + "fmt" + "path/filepath" + "runtime" + "strings" + "testing" +) + +const testFixture = "fixtures/foo.go" + +func TestParseEmptyInterface(t *testing.T) { + pkg, err := Parse(testFixture, "Fooer") + if err != nil { + t.Fatal(err) + } + + assertName(t, "foo", pkg.Name) + assertNum(t, 0, len(pkg.Functions)) +} + +func TestParseNonInterfaceType(t *testing.T) { + _, err := Parse(testFixture, "wobble") + if _, ok := err.(ErrUnexpectedType); !ok { + t.Fatal("expected type error when parsing non-interface type") + } +} + +func TestParseWithOneFunction(t *testing.T) { + pkg, err := Parse(testFixture, "Fooer2") + if err != nil { + t.Fatal(err) + } + + assertName(t, "foo", pkg.Name) + assertNum(t, 1, len(pkg.Functions)) + assertName(t, "Foo", pkg.Functions[0].Name) + assertNum(t, 0, len(pkg.Functions[0].Args)) + assertNum(t, 0, len(pkg.Functions[0].Returns)) +} + +func TestParseWithMultipleFuncs(t *testing.T) { + pkg, err := Parse(testFixture, "Fooer3") + if err != nil { + t.Fatal(err) + } + + assertName(t, "foo", pkg.Name) + assertNum(t, 6, len(pkg.Functions)) + + f := pkg.Functions[0] + assertName(t, "Foo", f.Name) + assertNum(t, 0, len(f.Args)) + assertNum(t, 0, len(f.Returns)) + + f = pkg.Functions[1] + assertName(t, "Bar", f.Name) + assertNum(t, 1, len(f.Args)) + assertNum(t, 0, len(f.Returns)) + arg := f.Args[0] + assertName(t, "a", arg.Name) + assertName(t, "string", arg.ArgType) + + f = pkg.Functions[2] + assertName(t, "Baz", f.Name) + assertNum(t, 1, len(f.Args)) + assertNum(t, 1, len(f.Returns)) + arg = f.Args[0] + assertName(t, "a", arg.Name) + assertName(t, "string", arg.ArgType) + arg = f.Returns[0] + assertName(t, "err", arg.Name) + assertName(t, "error", arg.ArgType) + + f = pkg.Functions[3] + assertName(t, "Qux", f.Name) + assertNum(t, 2, len(f.Args)) + assertNum(t, 2, len(f.Returns)) + arg = f.Args[0] + assertName(t, "a", f.Args[0].Name) + assertName(t, "string", f.Args[0].ArgType) + arg = f.Args[1] + assertName(t, "b", arg.Name) + assertName(t, "string", arg.ArgType) + arg = f.Returns[0] + assertName(t, "val", arg.Name) + assertName(t, "string", arg.ArgType) + arg = f.Returns[1] + assertName(t, "err", arg.Name) + assertName(t, "error", arg.ArgType) + + f = pkg.Functions[4] + assertName(t, "Wobble", f.Name) + assertNum(t, 0, len(f.Args)) + assertNum(t, 1, len(f.Returns)) + arg = f.Returns[0] + assertName(t, "w", arg.Name) + assertName(t, "*wobble", arg.ArgType) + + f = pkg.Functions[5] + assertName(t, "Wiggle", f.Name) + assertNum(t, 0, len(f.Args)) + assertNum(t, 1, len(f.Returns)) + arg = f.Returns[0] + assertName(t, "w", arg.Name) + assertName(t, "wobble", arg.ArgType) +} + +func TestParseWithUnamedReturn(t *testing.T) { + _, err := Parse(testFixture, "Fooer4") + if !strings.HasSuffix(err.Error(), ErrBadReturn.Error()) { + t.Fatalf("expected ErrBadReturn, got %v", err) + } +} + +func TestEmbeddedInterface(t *testing.T) { + pkg, err := Parse(testFixture, "Fooer5") + if err != nil { + t.Fatal(err) + } + + assertName(t, "foo", pkg.Name) + assertNum(t, 2, len(pkg.Functions)) + + f := pkg.Functions[0] + assertName(t, "Foo", f.Name) + assertNum(t, 0, len(f.Args)) + assertNum(t, 0, len(f.Returns)) + + f = pkg.Functions[1] + assertName(t, "Boo", f.Name) + assertNum(t, 2, len(f.Args)) + assertNum(t, 2, len(f.Returns)) + + arg := f.Args[0] + assertName(t, "a", arg.Name) + assertName(t, "string", arg.ArgType) + + arg = f.Args[1] + assertName(t, "b", arg.Name) + assertName(t, "string", arg.ArgType) + + arg = f.Returns[0] + assertName(t, "s", arg.Name) + assertName(t, "string", arg.ArgType) + + arg = f.Returns[1] + assertName(t, "err", arg.Name) + assertName(t, "error", arg.ArgType) +} + +func assertName(t *testing.T, expected, actual string) { + if expected != actual { + fatalOut(t, fmt.Sprintf("expected name to be `%s`, got: %s", expected, actual)) + } +} + +func assertNum(t *testing.T, expected, actual int) { + if expected != actual { + fatalOut(t, fmt.Sprintf("expected number to be %d, got: %d", expected, actual)) + } +} + +func fatalOut(t *testing.T, msg string) { + _, file, ln, _ := runtime.Caller(2) + t.Fatalf("%s:%d: %s", filepath.Base(file), ln, msg) +} diff --git a/pkg/plugins/pluginrpc-gen/template.go b/pkg/plugins/pluginrpc-gen/template.go new file mode 100644 index 0000000000..704030cf0f --- /dev/null +++ b/pkg/plugins/pluginrpc-gen/template.go @@ -0,0 +1,97 @@ +package main + +import ( + "strings" + "text/template" +) + +func printArgs(args []arg) string { + var argStr []string + for _, arg := range args { + argStr = append(argStr, arg.String()) + } + return strings.Join(argStr, ", ") +} + +func marshalType(t string) string { + switch t { + case "error": + // convert error types to plain strings to ensure the values are encoded/decoded properly + return "string" + default: + return t + } +} + +func isErr(t string) bool { + switch t { + case "error": + return true + default: + return false + } +} + +// Need to use this helper due to issues with go-vet +func buildTag(s string) string { + return "+build " + s +} + +var templFuncs = template.FuncMap{ + "printArgs": printArgs, + "marshalType": marshalType, + "isErr": isErr, + "lower": strings.ToLower, + "title": strings.Title, + "tag": buildTag, +} + +var generatedTempl = template.Must(template.New("rpc_cient").Funcs(templFuncs).Parse(` +// generated code - DO NOT EDIT +{{ range $k, $v := .BuildTags }} + // {{ tag $k }} {{ end }} + +package {{ .Name }} + +import "errors" + +type client interface{ + Call(string, interface{}, interface{}) error +} + +type {{ .InterfaceType }}Proxy struct { + client +} + +{{ range .Functions }} + type {{ $.InterfaceType }}Proxy{{ .Name }}Request struct{ + {{ range .Args }} + {{ title .Name }} {{ .ArgType }} {{ end }} + } + + type {{ $.InterfaceType }}Proxy{{ .Name }}Response struct{ + {{ range .Returns }} + {{ title .Name }} {{ marshalType .ArgType }} {{ end }} + } + + func (pp *{{ $.InterfaceType }}Proxy) {{ .Name }}({{ printArgs .Args }}) ({{ printArgs .Returns }}) { + var( + req {{ $.InterfaceType }}Proxy{{ .Name }}Request + ret {{ $.InterfaceType }}Proxy{{ .Name }}Response + ) + {{ range .Args }} + req.{{ title .Name }} = {{ lower .Name }} {{ end }} + if err = pp.Call("{{ $.RPCName }}.{{ .Name }}", req, &ret); err != nil { + return + } + {{ range $r := .Returns }} + {{ if isErr .ArgType }} + if ret.{{ title .Name }} != "" { + {{ lower .Name }} = errors.New(ret.{{ title .Name }}) + } {{ end }} + {{ if isErr .ArgType | not }} {{ lower .Name }} = ret.{{ title .Name }} {{ end }} {{ end }} + + return + } +{{ end }} +`)) From 88e4dff9a966485413d4acc1ecd99de6a6574e10 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Wed, 10 Jun 2015 09:46:54 -0400 Subject: [PATCH 2/2] use go-generate to build volume/driver/proxy.go Signed-off-by: Brian Goff --- volume/drivers/api.go | 11 ++- volume/drivers/proxy.go | 181 ++++++++++++++++++++++++++++------------ 2 files changed, 135 insertions(+), 57 deletions(-) diff --git a/volume/drivers/api.go b/volume/drivers/api.go index 1b98fa7fc5..3c685ae18d 100644 --- a/volume/drivers/api.go +++ b/volume/drivers/api.go @@ -1,20 +1,23 @@ +//go:generate pluginrpc-gen -i $GOFILE -o proxy.go -type VolumeDriver -name VolumeDriver + package volumedrivers import "github.com/docker/docker/volume" -type client interface { - Call(string, interface{}, interface{}) error -} - func NewVolumeDriver(name string, c client) volume.Driver { proxy := &volumeDriverProxy{c} return &volumeDriverAdapter{name, proxy} } type VolumeDriver interface { + // Create a volume with the given name Create(name string) (err error) + // Remove the volume with the given name Remove(name string) (err error) + // Get the mountpoint of the given volume Path(name string) (mountpoint string, err error) + // Mount the given volume and return the mountpoint Mount(name string) (mountpoint string, err error) + // Unmount the given volume Unmount(name string) (err error) } diff --git a/volume/drivers/proxy.go b/volume/drivers/proxy.go index c776175de9..9fd68855fe 100644 --- a/volume/drivers/proxy.go +++ b/volume/drivers/proxy.go @@ -1,74 +1,149 @@ +// generated code - DO NOT EDIT + package volumedrivers -import "fmt" +import "errors" -// currently created by hand. generation tool would generate this like: -// $ rpc-gen volume/drivers/api.go VolumeDriver > volume/drivers/proxy.go - -type volumeDriverRequest struct { - Name string -} - -type volumeDriverResponse struct { - Mountpoint string `json:",omitempty"` - Err string `json:",omitempty"` +type client interface { + Call(string, interface{}, interface{}) error } type volumeDriverProxy struct { - c client + client } -func (pp *volumeDriverProxy) Create(name string) error { - args := volumeDriverRequest{name} - var ret volumeDriverResponse - err := pp.c.Call("VolumeDriver.Create", args, &ret) - if err != nil { - return pp.fmtError(name, err.Error()) - } - return pp.fmtError(name, ret.Err) +type volumeDriverProxyCreateRequest struct { + Name string } -func (pp *volumeDriverProxy) Remove(name string) error { - args := volumeDriverRequest{name} - var ret volumeDriverResponse - err := pp.c.Call("VolumeDriver.Remove", args, &ret) - if err != nil { - return pp.fmtError(name, err.Error()) - } - return pp.fmtError(name, ret.Err) +type volumeDriverProxyCreateResponse struct { + Err string } -func (pp *volumeDriverProxy) Path(name string) (string, error) { - args := volumeDriverRequest{name} - var ret volumeDriverResponse - if err := pp.c.Call("VolumeDriver.Path", args, &ret); err != nil { - return "", pp.fmtError(name, err.Error()) +func (pp *volumeDriverProxy) Create(name string) (err error) { + var ( + req volumeDriverProxyCreateRequest + ret volumeDriverProxyCreateResponse + ) + + req.Name = name + if err = pp.Call("VolumeDriver.Create", req, &ret); err != nil { + return } - return ret.Mountpoint, pp.fmtError(name, ret.Err) + + if ret.Err != "" { + err = errors.New(ret.Err) + } + + return } -func (pp *volumeDriverProxy) Mount(name string) (string, error) { - args := volumeDriverRequest{name} - var ret volumeDriverResponse - if err := pp.c.Call("VolumeDriver.Mount", args, &ret); err != nil { - return "", pp.fmtError(name, err.Error()) - } - return ret.Mountpoint, pp.fmtError(name, ret.Err) +type volumeDriverProxyRemoveRequest struct { + Name string } -func (pp *volumeDriverProxy) Unmount(name string) error { - args := volumeDriverRequest{name} - var ret volumeDriverResponse - err := pp.c.Call("VolumeDriver.Unmount", args, &ret) - if err != nil { - return pp.fmtError(name, err.Error()) - } - return pp.fmtError(name, ret.Err) +type volumeDriverProxyRemoveResponse struct { + Err string } -func (pp *volumeDriverProxy) fmtError(name string, err string) error { - if len(err) == 0 { - return nil +func (pp *volumeDriverProxy) Remove(name string) (err error) { + var ( + req volumeDriverProxyRemoveRequest + ret volumeDriverProxyRemoveResponse + ) + + req.Name = name + if err = pp.Call("VolumeDriver.Remove", req, &ret); err != nil { + return } - return fmt.Errorf("External volume driver request failed for %s: %v", name, err) + + if ret.Err != "" { + err = errors.New(ret.Err) + } + + return +} + +type volumeDriverProxyPathRequest struct { + Name string +} + +type volumeDriverProxyPathResponse struct { + Mountpoint string + Err string +} + +func (pp *volumeDriverProxy) Path(name string) (mountpoint string, err error) { + var ( + req volumeDriverProxyPathRequest + ret volumeDriverProxyPathResponse + ) + + req.Name = name + if err = pp.Call("VolumeDriver.Path", req, &ret); err != nil { + return + } + + mountpoint = ret.Mountpoint + + if ret.Err != "" { + err = errors.New(ret.Err) + } + + return +} + +type volumeDriverProxyMountRequest struct { + Name string +} + +type volumeDriverProxyMountResponse struct { + Mountpoint string + Err string +} + +func (pp *volumeDriverProxy) Mount(name string) (mountpoint string, err error) { + var ( + req volumeDriverProxyMountRequest + ret volumeDriverProxyMountResponse + ) + + req.Name = name + if err = pp.Call("VolumeDriver.Mount", req, &ret); err != nil { + return + } + + mountpoint = ret.Mountpoint + + if ret.Err != "" { + err = errors.New(ret.Err) + } + + return +} + +type volumeDriverProxyUnmountRequest struct { + Name string +} + +type volumeDriverProxyUnmountResponse struct { + Err string +} + +func (pp *volumeDriverProxy) Unmount(name string) (err error) { + var ( + req volumeDriverProxyUnmountRequest + ret volumeDriverProxyUnmountResponse + ) + + req.Name = name + if err = pp.Call("VolumeDriver.Unmount", req, &ret); err != nil { + return + } + + if ret.Err != "" { + err = errors.New(ret.Err) + } + + return }