Initial commit

This commit is contained in:
Ria Bhatia
2017-12-04 13:32:57 -06:00
committed by Erik St. Martin
commit 0075e5b0f3
9056 changed files with 2523100 additions and 0 deletions

View File

@@ -0,0 +1,68 @@
Plugin RPC Generator
====================
Generates go code from a Go interface definition for proxying between the plugin
API and the subsystem being extended.
## Usage
Given an interface definition:
```go
type volumeDriver interface {
Create(name string, opts opts) (err error)
Remove(name string) (err error)
Path(name string) (mountpoint string, err error)
Mount(name string) (mountpoint string, err error)
Unmount(name string) (err error)
}
```
**Note**: All function options and return values must be named in the definition.
Run the generator:
```bash
$ pluginrpc-gen --type volumeDriver --name VolumeDriver -i volumes/drivers/extpoint.go -o volumes/drivers/proxy.go
```
Where:
- `--type` is the name of the interface to use
- `--name` is the subsystem that the plugin "Implements"
- `-i` is the input file containing the interface definition
- `-o` is the output file where the the generated code should go
**Note**: The generated code will use the same package name as the one defined in the input file
Optionally, you can skip functions on the interface that should not be
implemented in the generated proxy code by passing in the function name to `--skip`.
This flag can be specified multiple times.
You can also add build tags that should be prepended to the generated code by
supplying `--tag`. This flag can be specified multiple times.
## Known issues
The parser can currently only handle types which are not specifically a map or
a slice.
You can, however, create a type that uses a map or a slice internally, for instance:
```go
type opts map[string]string
```
This `opts` type will work, whreas using a `map[string]string` directly will not.
## go-generate
You can also use this with go-generate, which is pretty awesome.
To do so, place the code at the top of the file which contains the interface
definition (i.e., the input file):
```go
//go:generate pluginrpc-gen -i $GOFILE -o proxy.go -type volumeDriver -name VolumeDriver
```
Then cd to the package dir and run `go generate`
**Note**: the `pluginrpc-gen` binary must be within your `$PATH`

View File

@@ -0,0 +1,41 @@
package foo
type wobble struct {
Some string
Val string
Inception *wobble
}
// Fooer is an empty interface used for tests.
type Fooer interface{}
// Fooer2 is an interface used for tests.
type Fooer2 interface {
Foo()
}
// Fooer3 is an interface used for tests.
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)
}
// Fooer4 is an interface used for tests.
type Fooer4 interface {
Foo() error
}
// Bar is an interface used for tests.
type Bar interface {
Boo(a string, b string) (s string, err error)
}
// Fooer5 is an interface used for tests.
type Fooer5 interface {
Foo()
Bar
}

View File

@@ -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:]
}

View File

@@ -0,0 +1,163 @@
package main
import (
"errors"
"fmt"
"go/ast"
"go/parser"
"go/token"
"reflect"
)
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))
}
// ParsedPkg holds information about a package that has been parsed,
// its name and the list of functions.
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 a.Name + " " + a.ArgType
}
// Parse 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
}

View File

@@ -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)
}

View File

@@ -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 }}
`))