diff --git a/.travis.yml b/.travis.yml index 234cf3748..31624b2b4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,5 @@ script: - ./coveralls.bash go: - - 1.7.x - - 1.8.x + - 1.9.x - tip diff --git a/auth/basic/README.md b/auth/basic/README.md new file mode 100644 index 000000000..26d6c4b31 --- /dev/null +++ b/auth/basic/README.md @@ -0,0 +1,20 @@ +This package provides a Basic Authentication middleware. + +It'll try to compare credentials from Authentication request header to a username/password pair in middleware constructor. + +More details about this type of authentication can be found in [Mozilla article](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). + +## Usage + +```go +import httptransport "github.com/go-kit/kit/transport/http" + +httptransport.NewServer( + AuthMiddleware(cfg.auth.user, cfg.auth.password, "Example Realm")(makeUppercaseEndpoint()), + decodeMappingsRequest, + httptransport.EncodeJSONResponse, + httptransport.ServerBefore(httptransport.PopulateRequestContext), + ) +``` + +For AuthMiddleware to be able to pick up the Authentication header from an HTTP request we need to pass it through the context with something like ```httptransport.ServerBefore(httptransport.PopulateRequestContext)```. \ No newline at end of file diff --git a/auth/basic/middleware.go b/auth/basic/middleware.go new file mode 100644 index 000000000..ad7e4085d --- /dev/null +++ b/auth/basic/middleware.go @@ -0,0 +1,94 @@ +package basic + +import ( + "bytes" + "context" + "crypto/sha256" + "crypto/subtle" + "encoding/base64" + "fmt" + "net/http" + "strings" + + "github.com/go-kit/kit/endpoint" + httptransport "github.com/go-kit/kit/transport/http" +) + +// AuthError represents an authorization error. +type AuthError struct { + Realm string +} + +// StatusCode is an implementation of the StatusCoder interface in go-kit/http. +func (AuthError) StatusCode() int { + return http.StatusUnauthorized +} + +// Error is an implementation of the Error interface. +func (AuthError) Error() string { + return http.StatusText(http.StatusUnauthorized) +} + +// Headers is an implementation of the Headerer interface in go-kit/http. +func (e AuthError) Headers() http.Header { + return http.Header{ + "Content-Type": []string{"text/plain; charset=utf-8"}, + "X-Content-Type-Options": []string{"nosniff"}, + "WWW-Authenticate": []string{fmt.Sprintf(`Basic realm=%q`, e.Realm)}, + } +} + +// parseBasicAuth parses an HTTP Basic Authentication string. +// "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" returns ([]byte("Aladdin"), []byte("open sesame"), true). +func parseBasicAuth(auth string) (username, password []byte, ok bool) { + const prefix = "Basic " + if !strings.HasPrefix(auth, prefix) { + return + } + c, err := base64.StdEncoding.DecodeString(auth[len(prefix):]) + if err != nil { + return + } + + s := bytes.IndexByte(c, ':') + if s < 0 { + return + } + return c[:s], c[s+1:], true +} + +// Returns a hash of a given slice. +func toHashSlice(s []byte) []byte { + hash := sha256.Sum256(s) + return hash[:] +} + +// AuthMiddleware returns a Basic Authentication middleware for a particular user and password. +func AuthMiddleware(requiredUser, requiredPassword, realm string) endpoint.Middleware { + requiredUserBytes := toHashSlice([]byte(requiredUser)) + requiredPasswordBytes := toHashSlice([]byte(requiredPassword)) + + return func(next endpoint.Endpoint) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + auth, ok := ctx.Value(httptransport.ContextKeyRequestAuthorization).(string) + if !ok { + return nil, AuthError{realm} + } + + givenUser, givenPassword, ok := parseBasicAuth(auth) + if !ok { + return nil, AuthError{realm} + } + + givenUserBytes := toHashSlice(givenUser) + givenPasswordBytes := toHashSlice(givenPassword) + + if subtle.ConstantTimeCompare(givenUserBytes, requiredUserBytes) == 0 || + subtle.ConstantTimeCompare(givenPasswordBytes, requiredPasswordBytes) == 0 { + return nil, AuthError{realm} + } + + return next(ctx, request) + } + } +} diff --git a/auth/basic/middleware_test.go b/auth/basic/middleware_test.go new file mode 100644 index 000000000..9ad330ebb --- /dev/null +++ b/auth/basic/middleware_test.go @@ -0,0 +1,52 @@ +package basic + +import ( + "context" + "encoding/base64" + "fmt" + "testing" + + httptransport "github.com/go-kit/kit/transport/http" +) + +func TestWithBasicAuth(t *testing.T) { + requiredUser := "test-user" + requiredPassword := "test-pass" + realm := "test realm" + + type want struct { + result interface{} + err error + } + tests := []struct { + name string + authHeader interface{} + want want + }{ + {"Isn't valid with nil header", nil, want{nil, AuthError{realm}}}, + {"Isn't valid with non-string header", 42, want{nil, AuthError{realm}}}, + {"Isn't valid without authHeader", "", want{nil, AuthError{realm}}}, + {"Isn't valid for wrong user", makeAuthString("wrong-user", requiredPassword), want{nil, AuthError{realm}}}, + {"Isn't valid for wrong password", makeAuthString(requiredUser, "wrong-password"), want{nil, AuthError{realm}}}, + {"Is valid for correct creds", makeAuthString(requiredUser, requiredPassword), want{true, nil}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.WithValue(context.TODO(), httptransport.ContextKeyRequestAuthorization, tt.authHeader) + + result, err := AuthMiddleware(requiredUser, requiredPassword, realm)(passedValidation)(ctx, nil) + if result != tt.want.result || err != tt.want.err { + t.Errorf("WithBasicAuth() = result: %v, err: %v, want result: %v, want error: %v", result, err, tt.want.result, tt.want.err) + } + }) + } +} + +func makeAuthString(user string, password string) string { + data := []byte(fmt.Sprintf("%s:%s", user, password)) + return fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString(data)) +} + +func passedValidation(ctx context.Context, request interface{}) (response interface{}, err error) { + return true, nil +} diff --git a/auth/jwt/README.md b/auth/jwt/README.md index d435e0f1e..d2430bd28 100644 --- a/auth/jwt/README.md +++ b/auth/jwt/README.md @@ -13,7 +13,7 @@ will be added to the context via the `jwt.JWTClaimsContextKey`. ```go import ( stdjwt "github.com/dgrijalva/jwt-go" - + "github.com/go-kit/kit/auth/jwt" "github.com/go-kit/kit/endpoint" ) @@ -23,7 +23,7 @@ func main() { { kf := func(token *stdjwt.Token) (interface{}, error) { return []byte("SigningString"), nil } exampleEndpoint = MakeExampleEndpoint(service) - exampleEndpoint = jwt.NewParser(kf, stdjwt.SigningMethodHS256)(exampleEndpoint) + exampleEndpoint = jwt.NewParser(kf, stdjwt.SigningMethodHS256, jwt.StandardClaimsFactory)(exampleEndpoint) } } ``` @@ -35,7 +35,7 @@ the token string and add it to the context via the `jwt.JWTTokenContextKey`. ```go import ( stdjwt "github.com/dgrijalva/jwt-go" - + "github.com/go-kit/kit/auth/jwt" "github.com/go-kit/kit/endpoint" ) @@ -45,9 +45,9 @@ func main() { { exampleEndpoint = grpctransport.NewClient(...).Endpoint() exampleEndpoint = jwt.NewSigner( - "kid-header", - []byte("SigningString"), - stdjwt.SigningMethodHS256, + "kid-header", + []byte("SigningString"), + stdjwt.SigningMethodHS256, jwt.Claims{}, )(exampleEndpoint) } @@ -67,7 +67,7 @@ Example of use in a client: import ( stdjwt "github.com/dgrijalva/jwt-go" - grpctransport "github.com/go-kit/kit/transport/grpc" + grpctransport "github.com/go-kit/kit/transport/grpc" "github.com/go-kit/kit/auth/jwt" "github.com/go-kit/kit/endpoint" ) diff --git a/auth/jwt/middleware.go b/auth/jwt/middleware.go index c07d6e97e..0e29e6d68 100644 --- a/auth/jwt/middleware.go +++ b/auth/jwt/middleware.go @@ -70,21 +70,21 @@ func NewSigner(kid string, key []byte, method jwt.SigningMethod, claims jwt.Clai // Useful in NewParser middleware. type ClaimsFactory func() jwt.Claims -// MapClaimsFactory is a ClaimsFactory that returns +// MapClaimsFactory is a ClaimsFactory that returns // an empty jwt.MapClaims. func MapClaimsFactory() jwt.Claims { - return jwt.MapClaims{} + return jwt.MapClaims{} } -// StandardClaimsFactory is a ClaimsFactory that returns +// StandardClaimsFactory is a ClaimsFactory that returns // an empty jwt.StandardClaims. func StandardClaimsFactory() jwt.Claims { - return &jwt.StandardClaims{} + return &jwt.StandardClaims{} } // NewParser creates a new JWT token parsing middleware, specifying a // jwt.Keyfunc interface, the signing method and the claims type to be used. NewParser -// adds the resulting claims to endpoint context or returns error on invalid token. +// adds the resulting claims to endpoint context or returns error on invalid token. // Particularly useful for servers. func NewParser(keyFunc jwt.Keyfunc, method jwt.SigningMethod, newClaims ClaimsFactory) endpoint.Middleware { return func(next endpoint.Endpoint) endpoint.Endpoint { diff --git a/circle.yml b/circle.yml index 29520e694..35ace2ce8 100644 --- a/circle.yml +++ b/circle.yml @@ -2,7 +2,7 @@ machine: pre: - curl -sSL https://s3.amazonaws.com/circle-downloads/install-circleci-docker.sh | bash -s -- 1.10.0 - sudo rm -rf /usr/local/go - - curl -sSL https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz | sudo tar xz -C /usr/local + - curl -sSL https://storage.googleapis.com/golang/go1.9.linux-amd64.tar.gz | sudo tar xz -C /usr/local services: - docker diff --git a/cmd/kitgen/.ignore b/cmd/kitgen/.ignore new file mode 100644 index 000000000..747f955ca --- /dev/null +++ b/cmd/kitgen/.ignore @@ -0,0 +1 @@ +testdata/*/*/ diff --git a/cmd/kitgen/arg.go b/cmd/kitgen/arg.go new file mode 100644 index 000000000..bcf4e0a5d --- /dev/null +++ b/cmd/kitgen/arg.go @@ -0,0 +1,36 @@ +package main + +import "go/ast" + +type arg struct { + name, asField *ast.Ident + typ ast.Expr +} + +func (a arg) chooseName(scope *ast.Scope) *ast.Ident { + if a.name == nil || scope.Lookup(a.name.Name) != nil { + return inventName(a.typ, scope) + } + return a.name +} + +func (a arg) field(scope *ast.Scope) *ast.Field { + return &ast.Field{ + Names: []*ast.Ident{a.chooseName(scope)}, + Type: a.typ, + } +} + +func (a arg) result() *ast.Field { + return &ast.Field{ + Names: nil, + Type: a.typ, + } +} + +func (a arg) exported() *ast.Field { + return &ast.Field{ + Names: []*ast.Ident{id(export(a.asField.Name))}, + Type: a.typ, + } +} diff --git a/cmd/kitgen/ast_helpers.go b/cmd/kitgen/ast_helpers.go new file mode 100644 index 000000000..ab7c277db --- /dev/null +++ b/cmd/kitgen/ast_helpers.go @@ -0,0 +1,208 @@ +package main + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "strings" + "unicode" +) + +func export(s string) string { + return strings.Title(s) +} + +func unexport(s string) string { + first := true + return strings.Map(func(r rune) rune { + if first { + first = false + return unicode.ToLower(r) + } + return r + }, s) +} + +func inventName(t ast.Expr, scope *ast.Scope) *ast.Ident { + n := baseName(t) + for try := 0; ; try++ { + nstr := pickName(n, try) + obj := ast.NewObj(ast.Var, nstr) + if alt := scope.Insert(obj); alt == nil { + return ast.NewIdent(nstr) + } + } +} + +func baseName(t ast.Expr) string { + switch tt := t.(type) { + default: + panic(fmt.Sprintf("don't know how to choose a base name for %T (%[1]v)", tt)) + case *ast.ArrayType: + return "slice" + case *ast.Ident: + return tt.Name + case *ast.SelectorExpr: + return tt.Sel.Name + } +} + +func pickName(base string, idx int) string { + if idx == 0 { + switch base { + default: + return strings.Split(base, "")[0] + case "Context": + return "ctx" + case "error": + return "err" + } + } + return fmt.Sprintf("%s%d", base, idx) +} + +func scopeWith(names ...string) *ast.Scope { + scope := ast.NewScope(nil) + for _, name := range names { + scope.Insert(ast.NewObj(ast.Var, name)) + } + return scope +} + +type visitFn func(ast.Node, func(ast.Node)) + +func (fn visitFn) Visit(node ast.Node, r func(ast.Node)) Visitor { + fn(node, r) + return fn +} + +func replaceIdent(src ast.Node, named string, with ast.Node) ast.Node { + r := visitFn(func(node ast.Node, replaceWith func(ast.Node)) { + switch id := node.(type) { + case *ast.Ident: + if id.Name == named { + replaceWith(with) + } + } + }) + return WalkReplace(r, src) +} + +func replaceLit(src ast.Node, from, to string) ast.Node { + r := visitFn(func(node ast.Node, replaceWith func(ast.Node)) { + switch lit := node.(type) { + case *ast.BasicLit: + if lit.Value == from { + replaceWith(&ast.BasicLit{Value: to}) + } + } + }) + return WalkReplace(r, src) +} + +func fullAST() *ast.File { + full, err := ASTTemplates.Open("full.go") + if err != nil { + panic(err) + } + f, err := parser.ParseFile(token.NewFileSet(), "templates/full.go", full, parser.DeclarationErrors) + if err != nil { + panic(err) + } + return f +} + +func fetchImports() []*ast.ImportSpec { + return fullAST().Imports +} + +func fetchFuncDecl(name string) *ast.FuncDecl { + f := fullAST() + for _, decl := range f.Decls { + if f, ok := decl.(*ast.FuncDecl); ok && f.Name.Name == name { + return f + } + } + panic(fmt.Errorf("No function called %q in 'templates/full.go'", name)) +} + +func id(name string) *ast.Ident { + return ast.NewIdent(name) +} + +func sel(ids ...*ast.Ident) ast.Expr { + switch len(ids) { + default: + return &ast.SelectorExpr{ + X: sel(ids[:len(ids)-1]...), + Sel: ids[len(ids)-1], + } + case 1: + return ids[0] + case 0: + panic("zero ids to sel()") + } +} + +func typeField(t ast.Expr) *ast.Field { + return &ast.Field{Type: t} +} + +func field(n *ast.Ident, t ast.Expr) *ast.Field { + return &ast.Field{ + Names: []*ast.Ident{n}, + Type: t, + } +} + +func fieldList(list ...*ast.Field) *ast.FieldList { + return &ast.FieldList{List: list} +} + +func mappedFieldList(fn func(arg) *ast.Field, args ...arg) *ast.FieldList { + fl := &ast.FieldList{List: []*ast.Field{}} + for _, a := range args { + fl.List = append(fl.List, fn(a)) + } + return fl +} + +func blockStmt(stmts ...ast.Stmt) *ast.BlockStmt { + return &ast.BlockStmt{ + List: stmts, + } +} + +func structDecl(name *ast.Ident, fields *ast.FieldList) ast.Decl { + return typeDecl(&ast.TypeSpec{ + Name: name, + Type: &ast.StructType{ + Fields: fields, + }, + }) +} + +func typeDecl(ts *ast.TypeSpec) ast.Decl { + return &ast.GenDecl{ + Tok: token.TYPE, + Specs: []ast.Spec{ts}, + } +} + +func pasteStmts(body *ast.BlockStmt, idx int, stmts []ast.Stmt) { + list := body.List + prefix := list[:idx] + suffix := make([]ast.Stmt, len(list)-idx-1) + copy(suffix, list[idx+1:]) + + body.List = append(append(prefix, stmts...), suffix...) +} + +func importFor(is *ast.ImportSpec) *ast.GenDecl { + return &ast.GenDecl{Tok: token.IMPORT, Specs: []ast.Spec{is}} +} + +func importSpec(path string) *ast.ImportSpec { + return &ast.ImportSpec{Path: &ast.BasicLit{Kind: token.STRING, Value: `"` + path + `"`}} +} diff --git a/cmd/kitgen/ast_templates.go b/cmd/kitgen/ast_templates.go new file mode 100644 index 000000000..13aa87c20 --- /dev/null +++ b/cmd/kitgen/ast_templates.go @@ -0,0 +1,11 @@ +// This file was automatically generated based on the contents of *.tmpl +// If you need to update this file, change the contents of those files +// (or add new ones) and run 'go generate' + +package main + +import "golang.org/x/tools/godoc/vfs/mapfs" + +var ASTTemplates = mapfs.New(map[string]string{ + `full.go`: "package foo\n\nimport (\n \"context\"\n \"encoding/json\"\n \"errors\"\n \"net/http\"\n\n \"github.com/go-kit/kit/endpoint\"\n httptransport \"github.com/go-kit/kit/transport/http\"\n)\n\ntype ExampleService struct {\n}\n\ntype ExampleRequest struct {\n I int\n S string\n}\ntype ExampleResponse struct {\n S string\n Err error\n}\n\ntype Endpoints struct {\n ExampleEndpoint endpoint.Endpoint\n}\n\nfunc (f ExampleService) ExampleEndpoint(ctx context.Context, i int, s string) (string, error) {\n panic(errors.New(\"not implemented\"))\n}\n\nfunc makeExampleEndpoint(f ExampleService) endpoint.Endpoint {\n return func(ctx context.Context, request interface{}) (interface{}, error) {\n req := request.(ExampleRequest)\n s, err := f.ExampleEndpoint(ctx, req.I, req.S)\n return ExampleResponse{S: s, Err: err}, nil\n }\n}\n\nfunc inlineHandlerBuilder(m *http.ServeMux, endpoints Endpoints) {\n m.Handle(\"/bar\", httptransport.NewServer(endpoints.ExampleEndpoint, DecodeExampleRequest, EncodeExampleResponse))\n}\n\nfunc NewHTTPHandler(endpoints Endpoints) http.Handler {\n m := http.NewServeMux()\n inlineHandlerBuilder(m, endpoints)\n return m\n}\n\nfunc DecodeExampleRequest(_ context.Context, r *http.Request) (interface{}, error) {\n var req ExampleRequest\n err := json.NewDecoder(r.Body).Decode(&req)\n return req, err\n}\n\nfunc EncodeExampleResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {\n w.Header().Set(\"Content-Type\", \"application/json; charset=utf-8\")\n return json.NewEncoder(w).Encode(response)\n}\n", +}) diff --git a/cmd/kitgen/deflayout.go b/cmd/kitgen/deflayout.go new file mode 100644 index 000000000..27e2fec37 --- /dev/null +++ b/cmd/kitgen/deflayout.go @@ -0,0 +1,63 @@ +package main + +import "path/filepath" + +type deflayout struct { + targetDir string +} + +func (l deflayout) packagePath(sub string) string { + return filepath.Join(l.targetDir, sub) +} + +func (l deflayout) transformAST(ctx *sourceContext) (files, error) { + out := make(outputTree) + + endpoints := out.addFile("endpoints/endpoints.go", "endpoints") + http := out.addFile("http/http.go", "http") + service := out.addFile("service/service.go", "service") + + addImports(endpoints, ctx) + addImports(http, ctx) + addImports(service, ctx) + + for _, typ := range ctx.types { + addType(service, typ) + } + + for _, iface := range ctx.interfaces { //only one... + addStubStruct(service, iface) + + for _, meth := range iface.methods { + addMethod(service, iface, meth) + addRequestStruct(endpoints, meth) + addResponseStruct(endpoints, meth) + addEndpointMaker(endpoints, iface, meth) + } + + addEndpointsStruct(endpoints, iface) + addHTTPHandler(http, iface) + + for _, meth := range iface.methods { + addDecoder(http, meth) + addEncoder(http, meth) + } + + for name := range out { + out[name] = selectify(out[name], "service", iface.stubName().Name, l.packagePath("service")) + for _, meth := range iface.methods { + out[name] = selectify(out[name], "endpoints", meth.requestStructName().Name, l.packagePath("endpoints")) + } + } + } + + for name := range out { + out[name] = selectify(out[name], "endpoints", "Endpoints", l.packagePath("endpoints")) + + for _, typ := range ctx.types { + out[name] = selectify(out[name], "service", typ.Name.Name, l.packagePath("service")) + } + } + + return formatNodes(out) +} diff --git a/cmd/kitgen/flatlayout.go b/cmd/kitgen/flatlayout.go new file mode 100644 index 000000000..fedffa4b8 --- /dev/null +++ b/cmd/kitgen/flatlayout.go @@ -0,0 +1,39 @@ +package main + +import "go/ast" + +type flat struct{} + +func (f flat) transformAST(ctx *sourceContext) (files, error) { + root := &ast.File{ + Name: ctx.pkg, + Decls: []ast.Decl{}, + } + + addImports(root, ctx) + + for _, typ := range ctx.types { + addType(root, typ) + } + + for _, iface := range ctx.interfaces { //only one... + addStubStruct(root, iface) + + for _, meth := range iface.methods { + addMethod(root, iface, meth) + addRequestStruct(root, meth) + addResponseStruct(root, meth) + addEndpointMaker(root, iface, meth) + } + + addEndpointsStruct(root, iface) + addHTTPHandler(root, iface) + + for _, meth := range iface.methods { + addDecoder(root, meth) + addEncoder(root, meth) + } + } + + return formatNodes(outputTree{"gokit.go": root}) +} diff --git a/cmd/kitgen/interface.go b/cmd/kitgen/interface.go new file mode 100644 index 000000000..0c984dfca --- /dev/null +++ b/cmd/kitgen/interface.go @@ -0,0 +1,70 @@ +package main + +import "go/ast" + +// because "interface" is a keyword... +type iface struct { + name, stubname, rcvrName *ast.Ident + methods []method +} + +func (i iface) stubName() *ast.Ident { + return i.stubname +} + +func (i iface) stubStructDecl() ast.Decl { + return structDecl(i.stubName(), &ast.FieldList{}) +} + +func (i iface) endpointsStruct() ast.Decl { + fl := &ast.FieldList{} + for _, m := range i.methods { + fl.List = append(fl.List, &ast.Field{Names: []*ast.Ident{m.name}, Type: sel(id("endpoint"), id("Endpoint"))}) + } + return structDecl(id("Endpoints"), fl) +} + +func (i iface) httpHandler() ast.Decl { + handlerFn := fetchFuncDecl("NewHTTPHandler") + + // does this "inlining" process merit a helper akin to replaceIdent? + handleCalls := []ast.Stmt{} + for _, m := range i.methods { + handleCall := fetchFuncDecl("inlineHandlerBuilder").Body.List[0].(*ast.ExprStmt).X.(*ast.CallExpr) + + handleCall = replaceLit(handleCall, `"/bar"`, `"`+m.pathName()+`"`).(*ast.CallExpr) + handleCall = replaceIdent(handleCall, "ExampleEndpoint", m.name).(*ast.CallExpr) + handleCall = replaceIdent(handleCall, "DecodeExampleRequest", m.decodeFuncName()).(*ast.CallExpr) + handleCall = replaceIdent(handleCall, "EncodeExampleResponse", m.encodeFuncName()).(*ast.CallExpr) + + handleCalls = append(handleCalls, &ast.ExprStmt{X: handleCall}) + } + + pasteStmts(handlerFn.Body, 1, handleCalls) + + return handlerFn +} + +func (i iface) reciever() *ast.Field { + return field(i.receiverName(), i.stubName()) +} + +func (i iface) receiverName() *ast.Ident { + if i.rcvrName != nil { + return i.rcvrName + } + scope := ast.NewScope(nil) + for _, meth := range i.methods { + for _, arg := range meth.params { + if arg.name != nil { + scope.Insert(ast.NewObj(ast.Var, arg.name.Name)) + } + } + for _, arg := range meth.results { + if arg.name != nil { + scope.Insert(ast.NewObj(ast.Var, arg.name.Name)) + } + } + } + return id(unexport(inventName(i.name, scope).Name)) +} diff --git a/cmd/kitgen/main.go b/cmd/kitgen/main.go new file mode 100644 index 000000000..fdfd1fb9c --- /dev/null +++ b/cmd/kitgen/main.go @@ -0,0 +1,156 @@ +package main + +import ( + "flag" + "fmt" + "go/ast" + "go/parser" + "go/token" + "io" + "log" + "os" + "path" + + "github.com/pkg/errors" +) + +// go get github.com/nyarly/inlinefiles +//go:generate inlinefiles --package=main --vfs=ASTTemplates ./templates ast_templates.go + +func usage() string { + return fmt.Sprintf("Usage: %s (try -h)", os.Args[0]) +} + +var ( + help = flag.Bool("h", false, "print this help") + layoutkind = flag.String("repo-layout", "default", "default, flat...") + outdirrel = flag.String("target-dir", ".", "base directory to emit into") + //contextOmittable = flag.Bool("allow-no-context", false, "allow service methods to omit context parameter") +) + +func helpText() { + fmt.Println("USAGE") + fmt.Println(" kitgen [flags] path/to/service.go") + fmt.Println("") + fmt.Println("FLAGS") + flag.PrintDefaults() +} + +func main() { + flag.Parse() + + if *help { + helpText() + os.Exit(0) + } + + outdir := *outdirrel + if !path.IsAbs(*outdirrel) { + wd, err := os.Getwd() + if err != nil { + log.Fatalf("error getting current working directory: %v", err) + } + outdir = path.Join(wd, *outdirrel) + } + + var layout layout + switch *layoutkind { + default: + log.Fatalf("Unrecognized layout kind: %q - try 'default' or 'flat'", *layoutkind) + case "default": + gopath := getGopath() + importBase, err := importPath(outdir, gopath) + if err != nil { + log.Fatal(err) + } + layout = deflayout{targetDir: importBase} + case "flat": + layout = flat{} + } + + if len(os.Args) < 2 { + log.Fatal(usage()) + } + filename := flag.Arg(0) + file, err := os.Open(filename) + if err != nil { + log.Fatalf("error while opening %q: %v", filename, err) + } + + tree, err := process(filename, file, layout) + if err != nil { + log.Fatal(err) + } + + err = splat(outdir, tree) + if err != nil { + log.Fatal(err) + } +} + +func process(filename string, source io.Reader, layout layout) (files, error) { + f, err := parseFile(filename, source) + if err != nil { + return nil, errors.Wrapf(err, "parsing input %q", filename) + } + + context, err := extractContext(f) + if err != nil { + return nil, errors.Wrapf(err, "examining input file %q", filename) + } + + tree, err := layout.transformAST(context) + if err != nil { + return nil, errors.Wrapf(err, "generating AST") + } + return tree, nil +} + +/* + buf, err := formatNode(dest) + if err != nil { + return nil, errors.Wrapf(err, "formatting") + } + return buf, nil +} +*/ + +func parseFile(fname string, source io.Reader) (ast.Node, error) { + f, err := parser.ParseFile(token.NewFileSet(), fname, source, parser.DeclarationErrors) + if err != nil { + return nil, err + } + return f, nil +} + +func extractContext(f ast.Node) (*sourceContext, error) { + context := &sourceContext{} + visitor := &parseVisitor{src: context} + + ast.Walk(visitor, f) + + return context, context.validate() +} + +func splat(dir string, tree files) error { + for fn, buf := range tree { + if err := splatFile(path.Join(dir, fn), buf); err != nil { + return err + } + } + return nil +} + +func splatFile(target string, buf io.Reader) error { + err := os.MkdirAll(path.Dir(target), os.ModePerm) + if err != nil { + return errors.Wrapf(err, "Couldn't create directory for %q", target) + } + f, err := os.Create(target) + if err != nil { + return errors.Wrapf(err, "Couldn't create file %q", target) + } + defer f.Close() + _, err = io.Copy(f, buf) + return errors.Wrapf(err, "Error writing data to file %q", target) +} diff --git a/cmd/kitgen/main_test.go b/cmd/kitgen/main_test.go new file mode 100644 index 000000000..4ef5013a8 --- /dev/null +++ b/cmd/kitgen/main_test.go @@ -0,0 +1,113 @@ +package main + +import ( + "bytes" + "flag" + "fmt" + "io" + "io/ioutil" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +var update = flag.Bool("update", false, "update golden files") + +func TestProcess(t *testing.T) { + cases, err := filepath.Glob("testdata/*") + if err != nil { + t.Fatal(err) + } + + laidout := func(t *testing.T, inpath, dir, kind string, layout layout, in []byte) { + t.Run(kind, func(t *testing.T) { + targetDir := filepath.Join(dir, kind) + tree, err := process(inpath, bytes.NewBuffer(in), layout) + if err != nil { + t.Fatal(inpath, fmt.Sprintf("%+#v", err)) + } + + if *update { + err := splat(targetDir, tree) + if err != nil { + t.Fatal(kind, err) + } + // otherwise we need to do some tomfoolery with resetting buffers + // I'm willing to just run the tests again - besides, we shouldn't be + // regerating the golden files that often + t.Error("Updated outputs - DID NOT COMPARE! (run tests again without -update)") + return + } + + for filename, buf := range tree { + actual, err := ioutil.ReadAll(buf) + if err != nil { + t.Fatal(kind, filename, err) + } + + outpath := filepath.Join(targetDir, filename) + + expected, err := ioutil.ReadFile(outpath) + if err != nil { + t.Fatal(outpath, err) + } + + if !bytes.Equal(expected, actual) { + name := kind + filename + name = strings.Replace(name, "/", "-", -1) + + errfile, err := ioutil.TempFile("", name) + if err != nil { + t.Fatal("opening tempfile for output", err) + } + io.WriteString(errfile, string(actual)) + + diffCmd := exec.Command("diff", outpath, errfile.Name()) + diffOut, _ := diffCmd.Output() + t.Log(string(diffOut)) + t.Errorf("Processing output didn't match %q. Results recorded in %q.", outpath, errfile.Name()) + } + } + + if !t.Failed() { + build := exec.Command("go", "build", "./...") + build.Dir = targetDir + out, err := build.CombinedOutput() + if err != nil { + t.Fatalf("Cannot build output: %v\n%s", err, string(out)) + } + } + }) + + } + + testcase := func(dir string) { + name := filepath.Base(dir) + t.Run(name, func(t *testing.T) { + inpath := filepath.Join(dir, "in.go") + + in, err := ioutil.ReadFile(inpath) + if err != nil { + t.Fatal(inpath, err) + } + laidout(t, inpath, dir, "flat", flat{}, in) + laidout(t, inpath, dir, "default", deflayout{ + targetDir: filepath.Join("github.com/go-kit/kit/cmd/kitgen", dir, "default"), + }, in) + }) + } + + for _, dir := range cases { + testcase(dir) + } +} + +func TestTemplatesBuild(t *testing.T) { + build := exec.Command("go", "build", "./...") + build.Dir = "templates" + out, err := build.CombinedOutput() + if err != nil { + t.Fatal(err, "\n", string(out)) + } +} diff --git a/cmd/kitgen/method.go b/cmd/kitgen/method.go new file mode 100644 index 000000000..14238dd40 --- /dev/null +++ b/cmd/kitgen/method.go @@ -0,0 +1,220 @@ +package main + +import ( + "go/ast" + "go/token" + "strings" +) + +type method struct { + name *ast.Ident + params []arg + results []arg + structsResolved bool +} + +func (m method) definition(ifc iface) ast.Decl { + notImpl := fetchFuncDecl("ExampleEndpoint") + + notImpl.Name = m.name + notImpl.Recv = fieldList(ifc.reciever()) + scope := scopeWith(notImpl.Recv.List[0].Names[0].Name) + notImpl.Type.Params = m.funcParams(scope) + notImpl.Type.Results = m.funcResults() + + return notImpl +} + +func (m method) endpointMaker(ifc iface) ast.Decl { + endpointFn := fetchFuncDecl("makeExampleEndpoint") + scope := scopeWith("ctx", "req", ifc.receiverName().Name) + + anonFunc := endpointFn.Body.List[0].(*ast.ReturnStmt).Results[0].(*ast.FuncLit) + if !m.hasContext() { + // strip context param from endpoint function + anonFunc.Type.Params.List = anonFunc.Type.Params.List[1:] + } + + anonFunc = replaceIdent(anonFunc, "ExampleRequest", m.requestStructName()).(*ast.FuncLit) + callMethod := m.called(ifc, scope, "ctx", "req") + anonFunc.Body.List[1] = callMethod + anonFunc.Body.List[2].(*ast.ReturnStmt).Results[0] = m.wrapResult(callMethod.Lhs) + + endpointFn.Body.List[0].(*ast.ReturnStmt).Results[0] = anonFunc + endpointFn.Name = m.endpointMakerName() + endpointFn.Type.Params = fieldList(ifc.reciever()) + endpointFn.Type.Results = fieldList(typeField(sel(id("endpoint"), id("Endpoint")))) + return endpointFn +} + +func (m method) pathName() string { + return "/" + strings.ToLower(m.name.Name) +} + +func (m method) encodeFuncName() *ast.Ident { + return id("Encode" + m.name.Name + "Response") +} + +func (m method) decodeFuncName() *ast.Ident { + return id("Decode" + m.name.Name + "Request") +} + +func (m method) resultNames(scope *ast.Scope) []*ast.Ident { + ids := []*ast.Ident{} + for _, rz := range m.results { + ids = append(ids, rz.chooseName(scope)) + } + return ids +} + +func (m method) called(ifc iface, scope *ast.Scope, ctxName, spreadStruct string) *ast.AssignStmt { + m.resolveStructNames() + + resNamesExpr := []ast.Expr{} + for _, r := range m.resultNames(scope) { + resNamesExpr = append(resNamesExpr, ast.Expr(r)) + } + + arglist := []ast.Expr{} + if m.hasContext() { + arglist = append(arglist, id(ctxName)) + } + ssid := id(spreadStruct) + for _, f := range m.requestStructFields().List { + arglist = append(arglist, sel(ssid, f.Names[0])) + } + + return &ast.AssignStmt{ + Lhs: resNamesExpr, + Tok: token.DEFINE, + Rhs: []ast.Expr{ + &ast.CallExpr{ + Fun: sel(ifc.receiverName(), m.name), + Args: arglist, + }, + }, + } +} + +func (m method) wrapResult(results []ast.Expr) ast.Expr { + kvs := []ast.Expr{} + m.resolveStructNames() + + for i, a := range m.results { + kvs = append(kvs, &ast.KeyValueExpr{ + Key: ast.NewIdent(export(a.asField.Name)), + Value: results[i], + }) + } + return &ast.CompositeLit{ + Type: m.responseStructName(), + Elts: kvs, + } +} + +func (m method) resolveStructNames() { + if m.structsResolved { + return + } + m.structsResolved = true + scope := ast.NewScope(nil) + for i, p := range m.params { + p.asField = p.chooseName(scope) + m.params[i] = p + } + scope = ast.NewScope(nil) + for i, r := range m.results { + r.asField = r.chooseName(scope) + m.results[i] = r + } +} + +func (m method) decoderFunc() ast.Decl { + fn := fetchFuncDecl("DecodeExampleRequest") + fn.Name = m.decodeFuncName() + fn = replaceIdent(fn, "ExampleRequest", m.requestStructName()).(*ast.FuncDecl) + return fn +} + +func (m method) encoderFunc() ast.Decl { + fn := fetchFuncDecl("EncodeExampleResponse") + fn.Name = m.encodeFuncName() + return fn +} + +func (m method) endpointMakerName() *ast.Ident { + return id("make" + m.name.Name + "Endpoint") +} + +func (m method) requestStruct() ast.Decl { + m.resolveStructNames() + return structDecl(m.requestStructName(), m.requestStructFields()) +} + +func (m method) responseStruct() ast.Decl { + m.resolveStructNames() + return structDecl(m.responseStructName(), m.responseStructFields()) +} + +func (m method) hasContext() bool { + if len(m.params) < 1 { + return false + } + carg := m.params[0].typ + // ugh. this is maybe okay for the one-off, but a general case for matching + // types would be helpful + if sel, is := carg.(*ast.SelectorExpr); is && sel.Sel.Name == "Context" { + if id, is := sel.X.(*ast.Ident); is && id.Name == "context" { + return true + } + } + return false +} + +func (m method) nonContextParams() []arg { + if m.hasContext() { + return m.params[1:] + } + return m.params +} + +func (m method) funcParams(scope *ast.Scope) *ast.FieldList { + parms := &ast.FieldList{} + if m.hasContext() { + parms.List = []*ast.Field{{ + Names: []*ast.Ident{ast.NewIdent("ctx")}, + Type: sel(id("context"), id("Context")), + }} + scope.Insert(ast.NewObj(ast.Var, "ctx")) + } + parms.List = append(parms.List, mappedFieldList(func(a arg) *ast.Field { + return a.field(scope) + }, m.nonContextParams()...).List...) + return parms +} + +func (m method) funcResults() *ast.FieldList { + return mappedFieldList(func(a arg) *ast.Field { + return a.result() + }, m.results...) +} + +func (m method) requestStructName() *ast.Ident { + return id(export(m.name.Name) + "Request") +} + +func (m method) requestStructFields() *ast.FieldList { + return mappedFieldList(func(a arg) *ast.Field { + return a.exported() + }, m.nonContextParams()...) +} + +func (m method) responseStructName() *ast.Ident { + return id(export(m.name.Name) + "Response") +} + +func (m method) responseStructFields() *ast.FieldList { + return mappedFieldList(func(a arg) *ast.Field { + return a.exported() + }, m.results...) +} diff --git a/cmd/kitgen/parsevisitor.go b/cmd/kitgen/parsevisitor.go new file mode 100644 index 000000000..aa5131343 --- /dev/null +++ b/cmd/kitgen/parsevisitor.go @@ -0,0 +1,178 @@ +package main + +import ( + "go/ast" +) + +type ( + parseVisitor struct { + src *sourceContext + } + + typeSpecVisitor struct { + src *sourceContext + node *ast.TypeSpec + iface *iface + name *ast.Ident + } + + interfaceTypeVisitor struct { + node *ast.TypeSpec + ts *typeSpecVisitor + methods []method + } + + methodVisitor struct { + depth int + node *ast.TypeSpec + list *[]method + name *ast.Ident + params, results *[]arg + isMethod bool + } + + argListVisitor struct { + list *[]arg + } + + argVisitor struct { + node *ast.TypeSpec + parts []ast.Expr + list *[]arg + } +) + +func (v *parseVisitor) Visit(n ast.Node) ast.Visitor { + switch rn := n.(type) { + default: + return v + case *ast.File: + v.src.pkg = rn.Name + return v + case *ast.ImportSpec: + v.src.imports = append(v.src.imports, rn) + return nil + + case *ast.TypeSpec: + switch rn.Type.(type) { + default: + v.src.types = append(v.src.types, rn) + case *ast.InterfaceType: + // can't output interfaces + // because they'd conflict with our implementations + } + return &typeSpecVisitor{src: v.src, node: rn} + } +} + +/* +package foo + +type FooService interface { + Bar(ctx context.Context, i int, s string) (string, error) +} +*/ + +func (v *typeSpecVisitor) Visit(n ast.Node) ast.Visitor { + switch rn := n.(type) { + default: + return v + case *ast.Ident: + if v.name == nil { + v.name = rn + } + return v + case *ast.InterfaceType: + return &interfaceTypeVisitor{ts: v, methods: []method{}} + case nil: + if v.iface != nil { + v.iface.name = v.name + sn := *v.name + v.iface.stubname = &sn + v.iface.stubname.Name = v.name.String() + v.src.interfaces = append(v.src.interfaces, *v.iface) + } + return nil + } +} + +func (v *interfaceTypeVisitor) Visit(n ast.Node) ast.Visitor { + switch n.(type) { + default: + return v + case *ast.Field: + return &methodVisitor{list: &v.methods} + case nil: + v.ts.iface = &iface{methods: v.methods} + return nil + } +} + +func (v *methodVisitor) Visit(n ast.Node) ast.Visitor { + switch rn := n.(type) { + default: + v.depth++ + return v + case *ast.Ident: + if rn.IsExported() { + v.name = rn + } + v.depth++ + return v + case *ast.FuncType: + v.depth++ + v.isMethod = true + return v + case *ast.FieldList: + if v.params == nil { + v.params = &[]arg{} + return &argListVisitor{list: v.params} + } + if v.results == nil { + v.results = &[]arg{} + } + return &argListVisitor{list: v.results} + case nil: + v.depth-- + if v.depth == 0 && v.isMethod && v.name != nil { + *v.list = append(*v.list, method{name: v.name, params: *v.params, results: *v.results}) + } + return nil + } +} + +func (v *argListVisitor) Visit(n ast.Node) ast.Visitor { + switch n.(type) { + default: + return nil + case *ast.Field: + return &argVisitor{list: v.list} + } +} + +func (v *argVisitor) Visit(n ast.Node) ast.Visitor { + switch t := n.(type) { + case *ast.CommentGroup, *ast.BasicLit: + return nil + case *ast.Ident: //Expr -> everything, but clarity + if t.Name != "_" { + v.parts = append(v.parts, t) + } + case ast.Expr: + v.parts = append(v.parts, t) + case nil: + names := v.parts[:len(v.parts)-1] + tp := v.parts[len(v.parts)-1] + if len(names) == 0 { + *v.list = append(*v.list, arg{typ: tp}) + return nil + } + for _, n := range names { + *v.list = append(*v.list, arg{ + name: n.(*ast.Ident), + typ: tp, + }) + } + } + return nil +} diff --git a/cmd/kitgen/path_test.go b/cmd/kitgen/path_test.go new file mode 100644 index 000000000..371ded1d1 --- /dev/null +++ b/cmd/kitgen/path_test.go @@ -0,0 +1,43 @@ +package main + +import ( + "fmt" + "strings" + "testing" +) + +func TestImportPath(t *testing.T) { + testcase := func(gopath, targetpath, expected string) { + t.Run(fmt.Sprintf("%q + %q", gopath, targetpath), func(t *testing.T) { + actual, err := importPath(targetpath, gopath) + if err != nil { + t.Fatalf("Expected no error, got %q", err) + } + if actual != expected { + t.Errorf("Expected %q, got %q", expected, actual) + } + }) + } + + testcase("/gopath/", "/gopath/src/somewhere", "somewhere") + testcase("/gopath", "/gopath/src/somewhere", "somewhere") + testcase("/gopath:/other", "/gopath/src/somewhere", "somewhere") + testcase("/other:/gopath/", "/gopath/src/somewhere", "somewhere") +} + +func TestImportPathSadpath(t *testing.T) { + testcase := func(gopath, targetpath, expected string) { + t.Run(fmt.Sprintf("%q + %q", gopath, targetpath), func(t *testing.T) { + actual, err := importPath(targetpath, gopath) + if actual != "" { + t.Errorf("Expected empty path, got %q", actual) + } + if strings.Index(err.Error(), expected) == -1 { + t.Errorf("Expected %q to include %q", err, expected) + } + }) + } + + testcase("", "/gopath/src/somewhere", "is not in") + testcase("", "./somewhere", "not an absolute") +} diff --git a/cmd/kitgen/replacewalk.go b/cmd/kitgen/replacewalk.go new file mode 100644 index 000000000..f6b70dcd9 --- /dev/null +++ b/cmd/kitgen/replacewalk.go @@ -0,0 +1,759 @@ +package main + +import ( + "fmt" + "go/ast" +) + +// A Visitor's Visit method is invoked for each node encountered by walkToReplace. +// If the result visitor w is not nil, walkToReplace visits each of the children +// of node with the visitor w, followed by a call of w.Visit(nil). +type Visitor interface { + Visit(node ast.Node, replace func(ast.Node)) (w Visitor) +} + +// Helper functions for common node lists. They may be empty. + +func walkIdentList(v Visitor, list []*ast.Ident) { + for i, x := range list { + walkToReplace(v, x, func(r ast.Node) { + list[i] = r.(*ast.Ident) + }) + } +} + +func walkExprList(v Visitor, list []ast.Expr) { + for i, x := range list { + walkToReplace(v, x, func(r ast.Node) { + list[i] = r.(ast.Expr) + }) + } +} + +func walkStmtList(v Visitor, list []ast.Stmt) { + for i, x := range list { + walkToReplace(v, x, func(r ast.Node) { + list[i] = r.(ast.Stmt) + }) + } +} + +func walkDeclList(v Visitor, list []ast.Decl) { + for i, x := range list { + walkToReplace(v, x, func(r ast.Node) { + list[i] = r.(ast.Decl) + }) + } +} + +// WalkToReplace traverses an AST in depth-first order: It starts by calling +// v.Visit(node); node must not be nil. If the visitor w returned by +// v.Visit(node) is not nil, walkToReplace is invoked recursively with visitor +// w for each of the non-nil children of node, followed by a call of +// w.Visit(nil). +func WalkReplace(v Visitor, node ast.Node) (replacement ast.Node) { + walkToReplace(v, node, func(r ast.Node) { + replacement = r + }) + return +} + +func walkToReplace(v Visitor, node ast.Node, replace func(ast.Node)) { + if v == nil { + return + } + var replacement ast.Node + repl := func(r ast.Node) { + replacement = r + replace(r) + } + + v = v.Visit(node, repl) + + if replacement != nil { + return + } + + // walk children + // (the order of the cases matches the order + // of the corresponding node types in ast.go) + switch n := node.(type) { + + // These are all leaves, so there's no sub-walk to do. + // We just need to replace them on their parent with a copy. + case *ast.Comment: + cpy := *n + replace(&cpy) + case *ast.BadExpr: + cpy := *n + replace(&cpy) + case *ast.Ident: + cpy := *n + replace(&cpy) + case *ast.BasicLit: + cpy := *n + replace(&cpy) + case *ast.BadDecl: + cpy := *n + replace(&cpy) + case *ast.EmptyStmt: + cpy := *n + replace(&cpy) + case *ast.BadStmt: + cpy := *n + replace(&cpy) + + case *ast.CommentGroup: + cpy := *n + + if n.List != nil { + cpy.List = make([]*ast.Comment, len(n.List)) + copy(cpy.List, n.List) + } + + for i, c := range cpy.List { + walkToReplace(v, c, func(r ast.Node) { + cpy.List[i] = r.(*ast.Comment) + }) + } + replace(&cpy) + + case *ast.Field: + cpy := *n + if n.Names != nil { + cpy.Names = make([]*ast.Ident, len(n.Names)) + copy(cpy.Names, n.Names) + } + + if cpy.Doc != nil { + walkToReplace(v, cpy.Doc, func(r ast.Node) { + cpy.Doc = r.(*ast.CommentGroup) + }) + } + walkIdentList(v, cpy.Names) + + walkToReplace(v, cpy.Type, func(r ast.Node) { + cpy.Type = r.(ast.Expr) + }) + if cpy.Tag != nil { + walkToReplace(v, cpy.Tag, func(r ast.Node) { + cpy.Tag = r.(*ast.BasicLit) + }) + } + if cpy.Comment != nil { + walkToReplace(v, cpy.Comment, func(r ast.Node) { + cpy.Comment = r.(*ast.CommentGroup) + }) + } + replace(&cpy) + + case *ast.FieldList: + cpy := *n + if n.List != nil { + cpy.List = make([]*ast.Field, len(n.List)) + copy(cpy.List, n.List) + } + + for i, f := range cpy.List { + walkToReplace(v, f, func(r ast.Node) { + cpy.List[i] = r.(*ast.Field) + }) + } + + replace(&cpy) + + case *ast.Ellipsis: + cpy := *n + + if cpy.Elt != nil { + walkToReplace(v, cpy.Elt, func(r ast.Node) { + cpy.Elt = r.(ast.Expr) + }) + } + + replace(&cpy) + + case *ast.FuncLit: + cpy := *n + walkToReplace(v, cpy.Type, func(r ast.Node) { + cpy.Type = r.(*ast.FuncType) + }) + walkToReplace(v, cpy.Body, func(r ast.Node) { + cpy.Body = r.(*ast.BlockStmt) + }) + + replace(&cpy) + case *ast.CompositeLit: + cpy := *n + if n.Elts != nil { + cpy.Elts = make([]ast.Expr, len(n.Elts)) + copy(cpy.Elts, n.Elts) + } + + if cpy.Type != nil { + walkToReplace(v, cpy.Type, func(r ast.Node) { + cpy.Type = r.(ast.Expr) + }) + } + walkExprList(v, cpy.Elts) + + replace(&cpy) + case *ast.ParenExpr: + cpy := *n + walkToReplace(v, cpy.X, func(r ast.Node) { + cpy.X = r.(ast.Expr) + }) + + replace(&cpy) + case *ast.SelectorExpr: + cpy := *n + walkToReplace(v, cpy.X, func(r ast.Node) { + cpy.X = r.(ast.Expr) + }) + walkToReplace(v, cpy.Sel, func(r ast.Node) { + cpy.Sel = r.(*ast.Ident) + }) + + replace(&cpy) + case *ast.IndexExpr: + cpy := *n + walkToReplace(v, cpy.X, func(r ast.Node) { + cpy.X = r.(ast.Expr) + }) + walkToReplace(v, cpy.Index, func(r ast.Node) { + cpy.Index = r.(ast.Expr) + }) + + replace(&cpy) + case *ast.SliceExpr: + cpy := *n + walkToReplace(v, cpy.X, func(r ast.Node) { + cpy.X = r.(ast.Expr) + }) + if cpy.Low != nil { + walkToReplace(v, cpy.Low, func(r ast.Node) { + cpy.Low = r.(ast.Expr) + }) + } + if cpy.High != nil { + walkToReplace(v, cpy.High, func(r ast.Node) { + cpy.High = r.(ast.Expr) + }) + } + if cpy.Max != nil { + walkToReplace(v, cpy.Max, func(r ast.Node) { + cpy.Max = r.(ast.Expr) + }) + } + + replace(&cpy) + case *ast.TypeAssertExpr: + cpy := *n + walkToReplace(v, cpy.X, func(r ast.Node) { + cpy.X = r.(ast.Expr) + }) + if cpy.Type != nil { + walkToReplace(v, cpy.Type, func(r ast.Node) { + cpy.Type = r.(ast.Expr) + }) + } + replace(&cpy) + case *ast.CallExpr: + cpy := *n + if n.Args != nil { + cpy.Args = make([]ast.Expr, len(n.Args)) + copy(cpy.Args, n.Args) + } + + walkToReplace(v, cpy.Fun, func(r ast.Node) { + cpy.Fun = r.(ast.Expr) + }) + walkExprList(v, cpy.Args) + + replace(&cpy) + case *ast.StarExpr: + cpy := *n + walkToReplace(v, cpy.X, func(r ast.Node) { + cpy.X = r.(ast.Expr) + }) + + replace(&cpy) + case *ast.UnaryExpr: + cpy := *n + walkToReplace(v, cpy.X, func(r ast.Node) { + cpy.X = r.(ast.Expr) + }) + + replace(&cpy) + case *ast.BinaryExpr: + cpy := *n + walkToReplace(v, cpy.X, func(r ast.Node) { + cpy.X = r.(ast.Expr) + }) + walkToReplace(v, cpy.Y, func(r ast.Node) { + cpy.Y = r.(ast.Expr) + }) + + replace(&cpy) + case *ast.KeyValueExpr: + cpy := *n + walkToReplace(v, cpy.Key, func(r ast.Node) { + cpy.Key = r.(ast.Expr) + }) + walkToReplace(v, cpy.Value, func(r ast.Node) { + cpy.Value = r.(ast.Expr) + }) + + replace(&cpy) + + // Types + case *ast.ArrayType: + cpy := *n + if cpy.Len != nil { + walkToReplace(v, cpy.Len, func(r ast.Node) { + cpy.Len = r.(ast.Expr) + }) + } + walkToReplace(v, cpy.Elt, func(r ast.Node) { + cpy.Elt = r.(ast.Expr) + }) + + replace(&cpy) + case *ast.StructType: + cpy := *n + walkToReplace(v, cpy.Fields, func(r ast.Node) { + cpy.Fields = r.(*ast.FieldList) + }) + + replace(&cpy) + case *ast.FuncType: + cpy := *n + if cpy.Params != nil { + walkToReplace(v, cpy.Params, func(r ast.Node) { + cpy.Params = r.(*ast.FieldList) + }) + } + if cpy.Results != nil { + walkToReplace(v, cpy.Results, func(r ast.Node) { + cpy.Results = r.(*ast.FieldList) + }) + } + + replace(&cpy) + case *ast.InterfaceType: + cpy := *n + walkToReplace(v, cpy.Methods, func(r ast.Node) { + cpy.Methods = r.(*ast.FieldList) + }) + + replace(&cpy) + case *ast.MapType: + cpy := *n + walkToReplace(v, cpy.Key, func(r ast.Node) { + cpy.Key = r.(ast.Expr) + }) + walkToReplace(v, cpy.Value, func(r ast.Node) { + cpy.Value = r.(ast.Expr) + }) + + replace(&cpy) + case *ast.ChanType: + cpy := *n + walkToReplace(v, cpy.Value, func(r ast.Node) { + cpy.Value = r.(ast.Expr) + }) + + replace(&cpy) + case *ast.DeclStmt: + cpy := *n + walkToReplace(v, cpy.Decl, func(r ast.Node) { + cpy.Decl = r.(ast.Decl) + }) + + replace(&cpy) + case *ast.LabeledStmt: + cpy := *n + walkToReplace(v, cpy.Label, func(r ast.Node) { + cpy.Label = r.(*ast.Ident) + }) + walkToReplace(v, cpy.Stmt, func(r ast.Node) { + cpy.Stmt = r.(ast.Stmt) + }) + + replace(&cpy) + case *ast.ExprStmt: + cpy := *n + walkToReplace(v, cpy.X, func(r ast.Node) { + cpy.X = r.(ast.Expr) + }) + + replace(&cpy) + case *ast.SendStmt: + cpy := *n + walkToReplace(v, cpy.Chan, func(r ast.Node) { + cpy.Chan = r.(ast.Expr) + }) + walkToReplace(v, cpy.Value, func(r ast.Node) { + cpy.Value = r.(ast.Expr) + }) + + replace(&cpy) + case *ast.IncDecStmt: + cpy := *n + walkToReplace(v, cpy.X, func(r ast.Node) { + cpy.X = r.(ast.Expr) + }) + + replace(&cpy) + case *ast.AssignStmt: + cpy := *n + if n.Lhs != nil { + cpy.Lhs = make([]ast.Expr, len(n.Lhs)) + copy(cpy.Lhs, n.Lhs) + } + if n.Rhs != nil { + cpy.Rhs = make([]ast.Expr, len(n.Rhs)) + copy(cpy.Rhs, n.Rhs) + } + + walkExprList(v, cpy.Lhs) + walkExprList(v, cpy.Rhs) + + replace(&cpy) + case *ast.GoStmt: + cpy := *n + walkToReplace(v, cpy.Call, func(r ast.Node) { + cpy.Call = r.(*ast.CallExpr) + }) + + replace(&cpy) + case *ast.DeferStmt: + cpy := *n + walkToReplace(v, cpy.Call, func(r ast.Node) { + cpy.Call = r.(*ast.CallExpr) + }) + + replace(&cpy) + case *ast.ReturnStmt: + cpy := *n + if n.Results != nil { + cpy.Results = make([]ast.Expr, len(n.Results)) + copy(cpy.Results, n.Results) + } + + walkExprList(v, cpy.Results) + + replace(&cpy) + case *ast.BranchStmt: + cpy := *n + if cpy.Label != nil { + walkToReplace(v, cpy.Label, func(r ast.Node) { + cpy.Label = r.(*ast.Ident) + }) + } + + replace(&cpy) + case *ast.BlockStmt: + cpy := *n + if n.List != nil { + cpy.List = make([]ast.Stmt, len(n.List)) + copy(cpy.List, n.List) + } + + walkStmtList(v, cpy.List) + + replace(&cpy) + case *ast.IfStmt: + cpy := *n + + if cpy.Init != nil { + walkToReplace(v, cpy.Init, func(r ast.Node) { + cpy.Init = r.(ast.Stmt) + }) + } + walkToReplace(v, cpy.Cond, func(r ast.Node) { + cpy.Cond = r.(ast.Expr) + }) + walkToReplace(v, cpy.Body, func(r ast.Node) { + cpy.Body = r.(*ast.BlockStmt) + }) + if cpy.Else != nil { + walkToReplace(v, cpy.Else, func(r ast.Node) { + cpy.Else = r.(ast.Stmt) + }) + } + + replace(&cpy) + case *ast.CaseClause: + cpy := *n + if n.List != nil { + cpy.List = make([]ast.Expr, len(n.List)) + copy(cpy.List, n.List) + } + if n.Body != nil { + cpy.Body = make([]ast.Stmt, len(n.Body)) + copy(cpy.Body, n.Body) + } + + walkExprList(v, cpy.List) + walkStmtList(v, cpy.Body) + + replace(&cpy) + case *ast.SwitchStmt: + cpy := *n + if cpy.Init != nil { + walkToReplace(v, cpy.Init, func(r ast.Node) { + cpy.Init = r.(ast.Stmt) + }) + } + if cpy.Tag != nil { + walkToReplace(v, cpy.Tag, func(r ast.Node) { + cpy.Tag = r.(ast.Expr) + }) + } + walkToReplace(v, cpy.Body, func(r ast.Node) { + cpy.Body = r.(*ast.BlockStmt) + }) + + replace(&cpy) + case *ast.TypeSwitchStmt: + cpy := *n + if cpy.Init != nil { + walkToReplace(v, cpy.Init, func(r ast.Node) { + cpy.Init = r.(ast.Stmt) + }) + } + walkToReplace(v, cpy.Assign, func(r ast.Node) { + cpy.Assign = r.(ast.Stmt) + }) + walkToReplace(v, cpy.Body, func(r ast.Node) { + cpy.Body = r.(*ast.BlockStmt) + }) + + replace(&cpy) + case *ast.CommClause: + cpy := *n + if n.Body != nil { + cpy.Body = make([]ast.Stmt, len(n.Body)) + copy(cpy.Body, n.Body) + } + + if cpy.Comm != nil { + walkToReplace(v, cpy.Comm, func(r ast.Node) { + cpy.Comm = r.(ast.Stmt) + }) + } + walkStmtList(v, cpy.Body) + + replace(&cpy) + case *ast.SelectStmt: + cpy := *n + walkToReplace(v, cpy.Body, func(r ast.Node) { + cpy.Body = r.(*ast.BlockStmt) + }) + + replace(&cpy) + case *ast.ForStmt: + cpy := *n + if cpy.Init != nil { + walkToReplace(v, cpy.Init, func(r ast.Node) { + cpy.Init = r.(ast.Stmt) + }) + } + if cpy.Cond != nil { + walkToReplace(v, cpy.Cond, func(r ast.Node) { + cpy.Cond = r.(ast.Expr) + }) + } + if cpy.Post != nil { + walkToReplace(v, cpy.Post, func(r ast.Node) { + cpy.Post = r.(ast.Stmt) + }) + } + walkToReplace(v, cpy.Body, func(r ast.Node) { + cpy.Body = r.(*ast.BlockStmt) + }) + + replace(&cpy) + case *ast.RangeStmt: + cpy := *n + if cpy.Key != nil { + walkToReplace(v, cpy.Key, func(r ast.Node) { + cpy.Key = r.(ast.Expr) + }) + } + if cpy.Value != nil { + walkToReplace(v, cpy.Value, func(r ast.Node) { + cpy.Value = r.(ast.Expr) + }) + } + walkToReplace(v, cpy.X, func(r ast.Node) { + cpy.X = r.(ast.Expr) + }) + walkToReplace(v, cpy.Body, func(r ast.Node) { + cpy.Body = r.(*ast.BlockStmt) + }) + + // Declarations + replace(&cpy) + case *ast.ImportSpec: + cpy := *n + if cpy.Doc != nil { + walkToReplace(v, cpy.Doc, func(r ast.Node) { + cpy.Doc = r.(*ast.CommentGroup) + }) + } + if cpy.Name != nil { + walkToReplace(v, cpy.Name, func(r ast.Node) { + cpy.Name = r.(*ast.Ident) + }) + } + walkToReplace(v, cpy.Path, func(r ast.Node) { + cpy.Path = r.(*ast.BasicLit) + }) + if cpy.Comment != nil { + walkToReplace(v, cpy.Comment, func(r ast.Node) { + cpy.Comment = r.(*ast.CommentGroup) + }) + } + + replace(&cpy) + case *ast.ValueSpec: + cpy := *n + if n.Names != nil { + cpy.Names = make([]*ast.Ident, len(n.Names)) + copy(cpy.Names, n.Names) + } + if n.Values != nil { + cpy.Values = make([]ast.Expr, len(n.Values)) + copy(cpy.Values, n.Values) + } + + if cpy.Doc != nil { + walkToReplace(v, cpy.Doc, func(r ast.Node) { + cpy.Doc = r.(*ast.CommentGroup) + }) + } + + walkIdentList(v, cpy.Names) + + if cpy.Type != nil { + walkToReplace(v, cpy.Type, func(r ast.Node) { + cpy.Type = r.(ast.Expr) + }) + } + + walkExprList(v, cpy.Values) + + if cpy.Comment != nil { + walkToReplace(v, cpy.Comment, func(r ast.Node) { + cpy.Comment = r.(*ast.CommentGroup) + }) + } + + replace(&cpy) + + case *ast.TypeSpec: + cpy := *n + + if cpy.Doc != nil { + walkToReplace(v, cpy.Doc, func(r ast.Node) { + cpy.Doc = r.(*ast.CommentGroup) + }) + } + walkToReplace(v, cpy.Name, func(r ast.Node) { + cpy.Name = r.(*ast.Ident) + }) + walkToReplace(v, cpy.Type, func(r ast.Node) { + cpy.Type = r.(ast.Expr) + }) + if cpy.Comment != nil { + walkToReplace(v, cpy.Comment, func(r ast.Node) { + cpy.Comment = r.(*ast.CommentGroup) + }) + } + + replace(&cpy) + case *ast.GenDecl: + cpy := *n + if n.Specs != nil { + cpy.Specs = make([]ast.Spec, len(n.Specs)) + copy(cpy.Specs, n.Specs) + } + + if cpy.Doc != nil { + walkToReplace(v, cpy.Doc, func(r ast.Node) { + cpy.Doc = r.(*ast.CommentGroup) + }) + } + for i, s := range cpy.Specs { + walkToReplace(v, s, func(r ast.Node) { + cpy.Specs[i] = r.(ast.Spec) + }) + } + + replace(&cpy) + case *ast.FuncDecl: + cpy := *n + + if cpy.Doc != nil { + walkToReplace(v, cpy.Doc, func(r ast.Node) { + cpy.Doc = r.(*ast.CommentGroup) + }) + } + if cpy.Recv != nil { + walkToReplace(v, cpy.Recv, func(r ast.Node) { + cpy.Recv = r.(*ast.FieldList) + }) + } + walkToReplace(v, cpy.Name, func(r ast.Node) { + cpy.Name = r.(*ast.Ident) + }) + walkToReplace(v, cpy.Type, func(r ast.Node) { + cpy.Type = r.(*ast.FuncType) + }) + if cpy.Body != nil { + walkToReplace(v, cpy.Body, func(r ast.Node) { + cpy.Body = r.(*ast.BlockStmt) + }) + } + + // Files and packages + replace(&cpy) + case *ast.File: + cpy := *n + + if cpy.Doc != nil { + walkToReplace(v, cpy.Doc, func(r ast.Node) { + cpy.Doc = r.(*ast.CommentGroup) + }) + } + walkToReplace(v, cpy.Name, func(r ast.Node) { + cpy.Name = r.(*ast.Ident) + }) + walkDeclList(v, cpy.Decls) + // don't walk cpy.Comments - they have been + // visited already through the individual + // nodes + + replace(&cpy) + case *ast.Package: + cpy := *n + cpy.Files = map[string]*ast.File{} + + for i, f := range n.Files { + cpy.Files[i] = f + walkToReplace(v, f, func(r ast.Node) { + cpy.Files[i] = r.(*ast.File) + }) + } + replace(&cpy) + + default: + panic(fmt.Sprintf("walkToReplace: unexpected node type %T", n)) + } + + if v != nil { + v.Visit(nil, func(ast.Node) { panic("can't replace the go-up nil") }) + } +} diff --git a/cmd/kitgen/sourcecontext.go b/cmd/kitgen/sourcecontext.go new file mode 100644 index 000000000..35933a20f --- /dev/null +++ b/cmd/kitgen/sourcecontext.go @@ -0,0 +1,53 @@ +package main + +import ( + "fmt" + "go/ast" + "go/token" +) + +type sourceContext struct { + pkg *ast.Ident + imports []*ast.ImportSpec + interfaces []iface + types []*ast.TypeSpec +} + +func (sc *sourceContext) validate() error { + if len(sc.interfaces) != 1 { + return fmt.Errorf("found %d interfaces, expecting exactly 1", len(sc.interfaces)) + } + for _, i := range sc.interfaces { + for _, m := range i.methods { + if len(m.results) < 1 { + return fmt.Errorf("method %q of interface %q has no result types", m.name, i.name) + } + } + } + return nil +} + +func (sc *sourceContext) importDecls() (decls []ast.Decl) { + have := map[string]struct{}{} + notHave := func(is *ast.ImportSpec) bool { + if _, has := have[is.Path.Value]; has { + return false + } + have[is.Path.Value] = struct{}{} + return true + } + + for _, is := range sc.imports { + if notHave(is) { + decls = append(decls, importFor(is)) + } + } + + for _, is := range fetchImports() { + if notHave(is) { + decls = append(decls, &ast.GenDecl{Tok: token.IMPORT, Specs: []ast.Spec{is}}) + } + } + + return +} diff --git a/cmd/kitgen/templates/full.go b/cmd/kitgen/templates/full.go new file mode 100644 index 000000000..c3516856a --- /dev/null +++ b/cmd/kitgen/templates/full.go @@ -0,0 +1,60 @@ +package foo + +import ( + "context" + "encoding/json" + "errors" + "net/http" + + "github.com/go-kit/kit/endpoint" + httptransport "github.com/go-kit/kit/transport/http" +) + +type ExampleService struct { +} + +type ExampleRequest struct { + I int + S string +} +type ExampleResponse struct { + S string + Err error +} + +type Endpoints struct { + ExampleEndpoint endpoint.Endpoint +} + +func (f ExampleService) ExampleEndpoint(ctx context.Context, i int, s string) (string, error) { + panic(errors.New("not implemented")) +} + +func makeExampleEndpoint(f ExampleService) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(ExampleRequest) + s, err := f.ExampleEndpoint(ctx, req.I, req.S) + return ExampleResponse{S: s, Err: err}, nil + } +} + +func inlineHandlerBuilder(m *http.ServeMux, endpoints Endpoints) { + m.Handle("/bar", httptransport.NewServer(endpoints.ExampleEndpoint, DecodeExampleRequest, EncodeExampleResponse)) +} + +func NewHTTPHandler(endpoints Endpoints) http.Handler { + m := http.NewServeMux() + inlineHandlerBuilder(m, endpoints) + return m +} + +func DecodeExampleRequest(_ context.Context, r *http.Request) (interface{}, error) { + var req ExampleRequest + err := json.NewDecoder(r.Body).Decode(&req) + return req, err +} + +func EncodeExampleResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + return json.NewEncoder(w).Encode(response) +} diff --git a/cmd/kitgen/testdata/anonfields/default/endpoints/endpoints.go b/cmd/kitgen/testdata/anonfields/default/endpoints/endpoints.go new file mode 100644 index 000000000..b6902de65 --- /dev/null +++ b/cmd/kitgen/testdata/anonfields/default/endpoints/endpoints.go @@ -0,0 +1,28 @@ +package endpoints + +import "context" + +import "github.com/go-kit/kit/endpoint" + +import "github.com/go-kit/kit/cmd/kitgen/testdata/anonfields/default/service" + +type FooRequest struct { + I int + S string +} +type FooResponse struct { + I int + Err error +} + +func makeFooEndpoint(s service.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(FooRequest) + i, err := s.Foo(ctx, req.I, req.S) + return FooResponse{I: i, Err: err}, nil + } +} + +type Endpoints struct { + Foo endpoint.Endpoint +} diff --git a/cmd/kitgen/testdata/anonfields/default/http/http.go b/cmd/kitgen/testdata/anonfields/default/http/http.go new file mode 100644 index 000000000..e02944084 --- /dev/null +++ b/cmd/kitgen/testdata/anonfields/default/http/http.go @@ -0,0 +1,24 @@ +package http + +import "context" +import "encoding/json" + +import "net/http" + +import httptransport "github.com/go-kit/kit/transport/http" +import "github.com/go-kit/kit/cmd/kitgen/testdata/anonfields/default/endpoints" + +func NewHTTPHandler(endpoints endpoints.Endpoints) http.Handler { + m := http.NewServeMux() + m.Handle("/foo", httptransport.NewServer(endpoints.Foo, DecodeFooRequest, EncodeFooResponse)) + return m +} +func DecodeFooRequest(_ context.Context, r *http.Request) (interface{}, error) { + var req endpoints.FooRequest + err := json.NewDecoder(r.Body).Decode(&req) + return req, err +} +func EncodeFooResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + return json.NewEncoder(w).Encode(response) +} diff --git a/cmd/kitgen/testdata/anonfields/default/service/service.go b/cmd/kitgen/testdata/anonfields/default/service/service.go new file mode 100644 index 000000000..8adbd5a14 --- /dev/null +++ b/cmd/kitgen/testdata/anonfields/default/service/service.go @@ -0,0 +1,12 @@ +package service + +import "context" + +import "errors" + +type Service struct { +} + +func (s Service) Foo(ctx context.Context, i int, string1 string) (int, error) { + panic(errors.New("not implemented")) +} diff --git a/cmd/kitgen/testdata/anonfields/flat/gokit.go b/cmd/kitgen/testdata/anonfields/flat/gokit.go new file mode 100644 index 000000000..f19d2b275 --- /dev/null +++ b/cmd/kitgen/testdata/anonfields/flat/gokit.go @@ -0,0 +1,51 @@ +package foo + +import "context" +import "encoding/json" +import "errors" +import "net/http" +import "github.com/go-kit/kit/endpoint" +import httptransport "github.com/go-kit/kit/transport/http" + +type Service struct { +} + +func (s Service) Foo(ctx context.Context, i int, string1 string) (int, error) { + panic(errors.New("not implemented")) +} + +type FooRequest struct { + I int + S string +} +type FooResponse struct { + I int + Err error +} + +func makeFooEndpoint(s Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(FooRequest) + i, err := s.Foo(ctx, req.I, req.S) + return FooResponse{I: i, Err: err}, nil + } +} + +type Endpoints struct { + Foo endpoint.Endpoint +} + +func NewHTTPHandler(endpoints Endpoints) http.Handler { + m := http.NewServeMux() + m.Handle("/foo", httptransport.NewServer(endpoints.Foo, DecodeFooRequest, EncodeFooResponse)) + return m +} +func DecodeFooRequest(_ context.Context, r *http.Request) (interface{}, error) { + var req FooRequest + err := json.NewDecoder(r.Body).Decode(&req) + return req, err +} +func EncodeFooResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + return json.NewEncoder(w).Encode(response) +} diff --git a/cmd/kitgen/testdata/anonfields/in.go b/cmd/kitgen/testdata/anonfields/in.go new file mode 100644 index 000000000..c0c87d808 --- /dev/null +++ b/cmd/kitgen/testdata/anonfields/in.go @@ -0,0 +1,6 @@ +package foo + +// from https://github.com/go-kit/kit/pull/589#issuecomment-319937530 +type Service interface { + Foo(context.Context, int, string) (int, error) +} diff --git a/cmd/kitgen/testdata/foo/default/endpoints/endpoints.go b/cmd/kitgen/testdata/foo/default/endpoints/endpoints.go new file mode 100644 index 000000000..ff8ef0184 --- /dev/null +++ b/cmd/kitgen/testdata/foo/default/endpoints/endpoints.go @@ -0,0 +1,28 @@ +package endpoints + +import "context" + +import "github.com/go-kit/kit/endpoint" + +import "github.com/go-kit/kit/cmd/kitgen/testdata/foo/default/service" + +type BarRequest struct { + I int + S string +} +type BarResponse struct { + S string + Err error +} + +func makeBarEndpoint(f service.FooService) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(BarRequest) + s, err := f.Bar(ctx, req.I, req.S) + return BarResponse{S: s, Err: err}, nil + } +} + +type Endpoints struct { + Bar endpoint.Endpoint +} diff --git a/cmd/kitgen/testdata/foo/default/http/http.go b/cmd/kitgen/testdata/foo/default/http/http.go new file mode 100644 index 000000000..286926306 --- /dev/null +++ b/cmd/kitgen/testdata/foo/default/http/http.go @@ -0,0 +1,24 @@ +package http + +import "context" +import "encoding/json" + +import "net/http" + +import httptransport "github.com/go-kit/kit/transport/http" +import "github.com/go-kit/kit/cmd/kitgen/testdata/foo/default/endpoints" + +func NewHTTPHandler(endpoints endpoints.Endpoints) http.Handler { + m := http.NewServeMux() + m.Handle("/bar", httptransport.NewServer(endpoints.Bar, DecodeBarRequest, EncodeBarResponse)) + return m +} +func DecodeBarRequest(_ context.Context, r *http.Request) (interface{}, error) { + var req endpoints.BarRequest + err := json.NewDecoder(r.Body).Decode(&req) + return req, err +} +func EncodeBarResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + return json.NewEncoder(w).Encode(response) +} diff --git a/cmd/kitgen/testdata/foo/default/service/service.go b/cmd/kitgen/testdata/foo/default/service/service.go new file mode 100644 index 000000000..02a7babea --- /dev/null +++ b/cmd/kitgen/testdata/foo/default/service/service.go @@ -0,0 +1,12 @@ +package service + +import "context" + +import "errors" + +type FooService struct { +} + +func (f FooService) Bar(ctx context.Context, i int, s string) (string, error) { + panic(errors.New("not implemented")) +} diff --git a/cmd/kitgen/testdata/foo/flat/gokit.go b/cmd/kitgen/testdata/foo/flat/gokit.go new file mode 100644 index 000000000..9e0bc1f9b --- /dev/null +++ b/cmd/kitgen/testdata/foo/flat/gokit.go @@ -0,0 +1,51 @@ +package foo + +import "context" +import "encoding/json" +import "errors" +import "net/http" +import "github.com/go-kit/kit/endpoint" +import httptransport "github.com/go-kit/kit/transport/http" + +type FooService struct { +} + +func (f FooService) Bar(ctx context.Context, i int, s string) (string, error) { + panic(errors.New("not implemented")) +} + +type BarRequest struct { + I int + S string +} +type BarResponse struct { + S string + Err error +} + +func makeBarEndpoint(f FooService) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(BarRequest) + s, err := f.Bar(ctx, req.I, req.S) + return BarResponse{S: s, Err: err}, nil + } +} + +type Endpoints struct { + Bar endpoint.Endpoint +} + +func NewHTTPHandler(endpoints Endpoints) http.Handler { + m := http.NewServeMux() + m.Handle("/bar", httptransport.NewServer(endpoints.Bar, DecodeBarRequest, EncodeBarResponse)) + return m +} +func DecodeBarRequest(_ context.Context, r *http.Request) (interface{}, error) { + var req BarRequest + err := json.NewDecoder(r.Body).Decode(&req) + return req, err +} +func EncodeBarResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + return json.NewEncoder(w).Encode(response) +} diff --git a/cmd/kitgen/testdata/foo/in.go b/cmd/kitgen/testdata/foo/in.go new file mode 100644 index 000000000..1c01933fa --- /dev/null +++ b/cmd/kitgen/testdata/foo/in.go @@ -0,0 +1,5 @@ +package foo + +type FooService interface { + Bar(ctx context.Context, i int, s string) (string, error) +} diff --git a/cmd/kitgen/testdata/profilesvc/default/endpoints/endpoints.go b/cmd/kitgen/testdata/profilesvc/default/endpoints/endpoints.go new file mode 100644 index 000000000..892a1e1ff --- /dev/null +++ b/cmd/kitgen/testdata/profilesvc/default/endpoints/endpoints.go @@ -0,0 +1,162 @@ +package endpoints + +import "context" + +import "github.com/go-kit/kit/endpoint" + +import "github.com/go-kit/kit/cmd/kitgen/testdata/profilesvc/default/service" + +type PostProfileRequest struct { + P service.Profile +} +type PostProfileResponse struct { + Err error +} + +func makePostProfileEndpoint(s service.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(PostProfileRequest) + err := s.PostProfile(ctx, req.P) + return PostProfileResponse{Err: err}, nil + } +} + +type GetProfileRequest struct { + Id string +} +type GetProfileResponse struct { + P service.Profile + Err error +} + +func makeGetProfileEndpoint(s service.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(GetProfileRequest) + P, err := s.GetProfile(ctx, req.Id) + return GetProfileResponse{P: P, Err: err}, nil + } +} + +type PutProfileRequest struct { + Id string + P service.Profile +} +type PutProfileResponse struct { + Err error +} + +func makePutProfileEndpoint(s service.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(PutProfileRequest) + err := s.PutProfile(ctx, req.Id, req.P) + return PutProfileResponse{Err: err}, nil + } +} + +type PatchProfileRequest struct { + Id string + P service.Profile +} +type PatchProfileResponse struct { + Err error +} + +func makePatchProfileEndpoint(s service.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(PatchProfileRequest) + err := s.PatchProfile(ctx, req.Id, req.P) + return PatchProfileResponse{Err: err}, nil + } +} + +type DeleteProfileRequest struct { + Id string +} +type DeleteProfileResponse struct { + Err error +} + +func makeDeleteProfileEndpoint(s service.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(DeleteProfileRequest) + err := s.DeleteProfile(ctx, req.Id) + return DeleteProfileResponse{Err: err}, nil + } +} + +type GetAddressesRequest struct { + ProfileID string +} +type GetAddressesResponse struct { + S []service.Address + Err error +} + +func makeGetAddressesEndpoint(s service.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(GetAddressesRequest) + slice1, err := s.GetAddresses(ctx, req.ProfileID) + return GetAddressesResponse{S: slice1, Err: err}, nil + } +} + +type GetAddressRequest struct { + ProfileID string + AddressID string +} +type GetAddressResponse struct { + A service.Address + Err error +} + +func makeGetAddressEndpoint(s service.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(GetAddressRequest) + A, err := s.GetAddress(ctx, req.ProfileID, req.AddressID) + return GetAddressResponse{A: A, Err: err}, nil + } +} + +type PostAddressRequest struct { + ProfileID string + A service.Address +} +type PostAddressResponse struct { + Err error +} + +func makePostAddressEndpoint(s service.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(PostAddressRequest) + err := s.PostAddress(ctx, req.ProfileID, req.A) + return PostAddressResponse{Err: err}, nil + } +} + +type DeleteAddressRequest struct { + ProfileID string + AddressID string +} +type DeleteAddressResponse struct { + Err error +} + +func makeDeleteAddressEndpoint(s service.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(DeleteAddressRequest) + err := s.DeleteAddress(ctx, req.ProfileID, req.AddressID) + return DeleteAddressResponse{Err: err}, nil + } +} + +type Endpoints struct { + PostProfile endpoint.Endpoint + GetProfile endpoint.Endpoint + PutProfile endpoint.Endpoint + PatchProfile endpoint.Endpoint + DeleteProfile endpoint.Endpoint + GetAddresses endpoint.Endpoint + GetAddress endpoint.Endpoint + PostAddress endpoint.Endpoint + DeleteAddress endpoint.Endpoint +} diff --git a/cmd/kitgen/testdata/profilesvc/default/http/http.go b/cmd/kitgen/testdata/profilesvc/default/http/http.go new file mode 100644 index 000000000..b59f762b8 --- /dev/null +++ b/cmd/kitgen/testdata/profilesvc/default/http/http.go @@ -0,0 +1,104 @@ +package http + +import "context" +import "encoding/json" + +import "net/http" + +import httptransport "github.com/go-kit/kit/transport/http" +import "github.com/go-kit/kit/cmd/kitgen/testdata/profilesvc/default/endpoints" + +func NewHTTPHandler(endpoints endpoints.Endpoints) http.Handler { + m := http.NewServeMux() + m.Handle("/postprofile", httptransport.NewServer(endpoints.PostProfile, DecodePostProfileRequest, EncodePostProfileResponse)) + m.Handle("/getprofile", httptransport.NewServer(endpoints.GetProfile, DecodeGetProfileRequest, EncodeGetProfileResponse)) + m.Handle("/putprofile", httptransport.NewServer(endpoints.PutProfile, DecodePutProfileRequest, EncodePutProfileResponse)) + m.Handle("/patchprofile", httptransport.NewServer(endpoints.PatchProfile, DecodePatchProfileRequest, EncodePatchProfileResponse)) + m.Handle("/deleteprofile", httptransport.NewServer(endpoints.DeleteProfile, DecodeDeleteProfileRequest, EncodeDeleteProfileResponse)) + m.Handle("/getaddresses", httptransport.NewServer(endpoints.GetAddresses, DecodeGetAddressesRequest, EncodeGetAddressesResponse)) + m.Handle("/getaddress", httptransport.NewServer(endpoints.GetAddress, DecodeGetAddressRequest, EncodeGetAddressResponse)) + m.Handle("/postaddress", httptransport.NewServer(endpoints.PostAddress, DecodePostAddressRequest, EncodePostAddressResponse)) + m.Handle("/deleteaddress", httptransport.NewServer(endpoints.DeleteAddress, DecodeDeleteAddressRequest, EncodeDeleteAddressResponse)) + return m +} +func DecodePostProfileRequest(_ context.Context, r *http.Request) (interface{}, error) { + var req endpoints.PostProfileRequest + err := json.NewDecoder(r.Body).Decode(&req) + return req, err +} +func EncodePostProfileResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + return json.NewEncoder(w).Encode(response) +} +func DecodeGetProfileRequest(_ context.Context, r *http.Request) (interface{}, error) { + var req endpoints.GetProfileRequest + err := json.NewDecoder(r.Body).Decode(&req) + return req, err +} +func EncodeGetProfileResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + return json.NewEncoder(w).Encode(response) +} +func DecodePutProfileRequest(_ context.Context, r *http.Request) (interface{}, error) { + var req endpoints.PutProfileRequest + err := json.NewDecoder(r.Body).Decode(&req) + return req, err +} +func EncodePutProfileResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + return json.NewEncoder(w).Encode(response) +} +func DecodePatchProfileRequest(_ context.Context, r *http.Request) (interface{}, error) { + var req endpoints.PatchProfileRequest + err := json.NewDecoder(r.Body).Decode(&req) + return req, err +} +func EncodePatchProfileResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + return json.NewEncoder(w).Encode(response) +} +func DecodeDeleteProfileRequest(_ context.Context, r *http.Request) (interface{}, error) { + var req endpoints.DeleteProfileRequest + err := json.NewDecoder(r.Body).Decode(&req) + return req, err +} +func EncodeDeleteProfileResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + return json.NewEncoder(w).Encode(response) +} +func DecodeGetAddressesRequest(_ context.Context, r *http.Request) (interface{}, error) { + var req endpoints.GetAddressesRequest + err := json.NewDecoder(r.Body).Decode(&req) + return req, err +} +func EncodeGetAddressesResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + return json.NewEncoder(w).Encode(response) +} +func DecodeGetAddressRequest(_ context.Context, r *http.Request) (interface{}, error) { + var req endpoints.GetAddressRequest + err := json.NewDecoder(r.Body).Decode(&req) + return req, err +} +func EncodeGetAddressResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + return json.NewEncoder(w).Encode(response) +} +func DecodePostAddressRequest(_ context.Context, r *http.Request) (interface{}, error) { + var req endpoints.PostAddressRequest + err := json.NewDecoder(r.Body).Decode(&req) + return req, err +} +func EncodePostAddressResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + return json.NewEncoder(w).Encode(response) +} +func DecodeDeleteAddressRequest(_ context.Context, r *http.Request) (interface{}, error) { + var req endpoints.DeleteAddressRequest + err := json.NewDecoder(r.Body).Decode(&req) + return req, err +} +func EncodeDeleteAddressResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + return json.NewEncoder(w).Encode(response) +} diff --git a/cmd/kitgen/testdata/profilesvc/default/service/service.go b/cmd/kitgen/testdata/profilesvc/default/service/service.go new file mode 100644 index 000000000..42d1b4d2e --- /dev/null +++ b/cmd/kitgen/testdata/profilesvc/default/service/service.go @@ -0,0 +1,45 @@ +package service + +import "context" + +import "errors" + +type Profile struct { + ID string `json:"id"` + Name string `json:"name,omitempty"` + Addresses []Address `json:"addresses,omitempty"` +} +type Address struct { + ID string `json:"id"` + Location string `json:"location,omitempty"` +} +type Service struct { +} + +func (s Service) PostProfile(ctx context.Context, p Profile) error { + panic(errors.New("not implemented")) +} +func (s Service) GetProfile(ctx context.Context, id string) (Profile, error) { + panic(errors.New("not implemented")) +} +func (s Service) PutProfile(ctx context.Context, id string, p Profile) error { + panic(errors.New("not implemented")) +} +func (s Service) PatchProfile(ctx context.Context, id string, p Profile) error { + panic(errors.New("not implemented")) +} +func (s Service) DeleteProfile(ctx context.Context, id string) error { + panic(errors.New("not implemented")) +} +func (s Service) GetAddresses(ctx context.Context, profileID string) ([]Address, error) { + panic(errors.New("not implemented")) +} +func (s Service) GetAddress(ctx context.Context, profileID string, addressID string) (Address, error) { + panic(errors.New("not implemented")) +} +func (s Service) PostAddress(ctx context.Context, profileID string, a Address) error { + panic(errors.New("not implemented")) +} +func (s Service) DeleteAddress(ctx context.Context, profileID string, addressID string) error { + panic(errors.New("not implemented")) +} diff --git a/cmd/kitgen/testdata/profilesvc/flat/gokit.go b/cmd/kitgen/testdata/profilesvc/flat/gokit.go new file mode 100644 index 000000000..10fb436d1 --- /dev/null +++ b/cmd/kitgen/testdata/profilesvc/flat/gokit.go @@ -0,0 +1,298 @@ +package profilesvc + +import "context" +import "encoding/json" +import "errors" +import "net/http" +import "github.com/go-kit/kit/endpoint" +import httptransport "github.com/go-kit/kit/transport/http" + +type Profile struct { + ID string `json:"id"` + Name string `json:"name,omitempty"` + Addresses []Address `json:"addresses,omitempty"` +} +type Address struct { + ID string `json:"id"` + Location string `json:"location,omitempty"` +} +type Service struct { +} + +func (s Service) PostProfile(ctx context.Context, p Profile) error { + panic(errors.New("not implemented")) +} + +type PostProfileRequest struct { + P Profile +} +type PostProfileResponse struct { + Err error +} + +func makePostProfileEndpoint(s Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(PostProfileRequest) + err := s.PostProfile(ctx, req.P) + return PostProfileResponse{Err: err}, nil + } +} +func (s Service) GetProfile(ctx context.Context, id string) (Profile, error) { + panic(errors.New("not implemented")) +} + +type GetProfileRequest struct { + Id string +} +type GetProfileResponse struct { + P Profile + Err error +} + +func makeGetProfileEndpoint(s Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(GetProfileRequest) + P, err := s.GetProfile(ctx, req.Id) + return GetProfileResponse{P: P, Err: err}, nil + } +} +func (s Service) PutProfile(ctx context.Context, id string, p Profile) error { + panic(errors.New("not implemented")) +} + +type PutProfileRequest struct { + Id string + P Profile +} +type PutProfileResponse struct { + Err error +} + +func makePutProfileEndpoint(s Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(PutProfileRequest) + err := s.PutProfile(ctx, req.Id, req.P) + return PutProfileResponse{Err: err}, nil + } +} +func (s Service) PatchProfile(ctx context.Context, id string, p Profile) error { + panic(errors.New("not implemented")) +} + +type PatchProfileRequest struct { + Id string + P Profile +} +type PatchProfileResponse struct { + Err error +} + +func makePatchProfileEndpoint(s Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(PatchProfileRequest) + err := s.PatchProfile(ctx, req.Id, req.P) + return PatchProfileResponse{Err: err}, nil + } +} +func (s Service) DeleteProfile(ctx context.Context, id string) error { + panic(errors.New("not implemented")) +} + +type DeleteProfileRequest struct { + Id string +} +type DeleteProfileResponse struct { + Err error +} + +func makeDeleteProfileEndpoint(s Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(DeleteProfileRequest) + err := s.DeleteProfile(ctx, req.Id) + return DeleteProfileResponse{Err: err}, nil + } +} +func (s Service) GetAddresses(ctx context.Context, profileID string) ([]Address, error) { + panic(errors.New("not implemented")) +} + +type GetAddressesRequest struct { + ProfileID string +} +type GetAddressesResponse struct { + S []Address + Err error +} + +func makeGetAddressesEndpoint(s Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(GetAddressesRequest) + slice1, err := s.GetAddresses(ctx, req.ProfileID) + return GetAddressesResponse{S: slice1, Err: err}, nil + } +} +func (s Service) GetAddress(ctx context.Context, profileID string, addressID string) (Address, error) { + panic(errors.New("not implemented")) +} + +type GetAddressRequest struct { + ProfileID string + AddressID string +} +type GetAddressResponse struct { + A Address + Err error +} + +func makeGetAddressEndpoint(s Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(GetAddressRequest) + A, err := s.GetAddress(ctx, req.ProfileID, req.AddressID) + return GetAddressResponse{A: A, Err: err}, nil + } +} +func (s Service) PostAddress(ctx context.Context, profileID string, a Address) error { + panic(errors.New("not implemented")) +} + +type PostAddressRequest struct { + ProfileID string + A Address +} +type PostAddressResponse struct { + Err error +} + +func makePostAddressEndpoint(s Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(PostAddressRequest) + err := s.PostAddress(ctx, req.ProfileID, req.A) + return PostAddressResponse{Err: err}, nil + } +} +func (s Service) DeleteAddress(ctx context.Context, profileID string, addressID string) error { + panic(errors.New("not implemented")) +} + +type DeleteAddressRequest struct { + ProfileID string + AddressID string +} +type DeleteAddressResponse struct { + Err error +} + +func makeDeleteAddressEndpoint(s Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(DeleteAddressRequest) + err := s.DeleteAddress(ctx, req.ProfileID, req.AddressID) + return DeleteAddressResponse{Err: err}, nil + } +} + +type Endpoints struct { + PostProfile endpoint.Endpoint + GetProfile endpoint.Endpoint + PutProfile endpoint.Endpoint + PatchProfile endpoint.Endpoint + DeleteProfile endpoint.Endpoint + GetAddresses endpoint.Endpoint + GetAddress endpoint.Endpoint + PostAddress endpoint.Endpoint + DeleteAddress endpoint.Endpoint +} + +func NewHTTPHandler(endpoints Endpoints) http.Handler { + m := http.NewServeMux() + m.Handle("/postprofile", httptransport.NewServer(endpoints.PostProfile, DecodePostProfileRequest, EncodePostProfileResponse)) + m.Handle("/getprofile", httptransport.NewServer(endpoints.GetProfile, DecodeGetProfileRequest, EncodeGetProfileResponse)) + m.Handle("/putprofile", httptransport.NewServer(endpoints.PutProfile, DecodePutProfileRequest, EncodePutProfileResponse)) + m.Handle("/patchprofile", httptransport.NewServer(endpoints.PatchProfile, DecodePatchProfileRequest, EncodePatchProfileResponse)) + m.Handle("/deleteprofile", httptransport.NewServer(endpoints.DeleteProfile, DecodeDeleteProfileRequest, EncodeDeleteProfileResponse)) + m.Handle("/getaddresses", httptransport.NewServer(endpoints.GetAddresses, DecodeGetAddressesRequest, EncodeGetAddressesResponse)) + m.Handle("/getaddress", httptransport.NewServer(endpoints.GetAddress, DecodeGetAddressRequest, EncodeGetAddressResponse)) + m.Handle("/postaddress", httptransport.NewServer(endpoints.PostAddress, DecodePostAddressRequest, EncodePostAddressResponse)) + m.Handle("/deleteaddress", httptransport.NewServer(endpoints.DeleteAddress, DecodeDeleteAddressRequest, EncodeDeleteAddressResponse)) + return m +} +func DecodePostProfileRequest(_ context.Context, r *http.Request) (interface{}, error) { + var req PostProfileRequest + err := json.NewDecoder(r.Body).Decode(&req) + return req, err +} +func EncodePostProfileResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + return json.NewEncoder(w).Encode(response) +} +func DecodeGetProfileRequest(_ context.Context, r *http.Request) (interface{}, error) { + var req GetProfileRequest + err := json.NewDecoder(r.Body).Decode(&req) + return req, err +} +func EncodeGetProfileResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + return json.NewEncoder(w).Encode(response) +} +func DecodePutProfileRequest(_ context.Context, r *http.Request) (interface{}, error) { + var req PutProfileRequest + err := json.NewDecoder(r.Body).Decode(&req) + return req, err +} +func EncodePutProfileResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + return json.NewEncoder(w).Encode(response) +} +func DecodePatchProfileRequest(_ context.Context, r *http.Request) (interface{}, error) { + var req PatchProfileRequest + err := json.NewDecoder(r.Body).Decode(&req) + return req, err +} +func EncodePatchProfileResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + return json.NewEncoder(w).Encode(response) +} +func DecodeDeleteProfileRequest(_ context.Context, r *http.Request) (interface{}, error) { + var req DeleteProfileRequest + err := json.NewDecoder(r.Body).Decode(&req) + return req, err +} +func EncodeDeleteProfileResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + return json.NewEncoder(w).Encode(response) +} +func DecodeGetAddressesRequest(_ context.Context, r *http.Request) (interface{}, error) { + var req GetAddressesRequest + err := json.NewDecoder(r.Body).Decode(&req) + return req, err +} +func EncodeGetAddressesResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + return json.NewEncoder(w).Encode(response) +} +func DecodeGetAddressRequest(_ context.Context, r *http.Request) (interface{}, error) { + var req GetAddressRequest + err := json.NewDecoder(r.Body).Decode(&req) + return req, err +} +func EncodeGetAddressResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + return json.NewEncoder(w).Encode(response) +} +func DecodePostAddressRequest(_ context.Context, r *http.Request) (interface{}, error) { + var req PostAddressRequest + err := json.NewDecoder(r.Body).Decode(&req) + return req, err +} +func EncodePostAddressResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + return json.NewEncoder(w).Encode(response) +} +func DecodeDeleteAddressRequest(_ context.Context, r *http.Request) (interface{}, error) { + var req DeleteAddressRequest + err := json.NewDecoder(r.Body).Decode(&req) + return req, err +} +func EncodeDeleteAddressResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + return json.NewEncoder(w).Encode(response) +} diff --git a/cmd/kitgen/testdata/profilesvc/in.go b/cmd/kitgen/testdata/profilesvc/in.go new file mode 100644 index 000000000..208fed9b7 --- /dev/null +++ b/cmd/kitgen/testdata/profilesvc/in.go @@ -0,0 +1,24 @@ +package profilesvc + +type Service interface { + PostProfile(ctx context.Context, p Profile) error + GetProfile(ctx context.Context, id string) (Profile, error) + PutProfile(ctx context.Context, id string, p Profile) error + PatchProfile(ctx context.Context, id string, p Profile) error + DeleteProfile(ctx context.Context, id string) error + GetAddresses(ctx context.Context, profileID string) ([]Address, error) + GetAddress(ctx context.Context, profileID string, addressID string) (Address, error) + PostAddress(ctx context.Context, profileID string, a Address) error + DeleteAddress(ctx context.Context, profileID string, addressID string) error +} + +type Profile struct { + ID string `json:"id"` + Name string `json:"name,omitempty"` + Addresses []Address `json:"addresses,omitempty"` +} + +type Address struct { + ID string `json:"id"` + Location string `json:"location,omitempty"` +} diff --git a/cmd/kitgen/testdata/stringservice/default/endpoints/endpoints.go b/cmd/kitgen/testdata/stringservice/default/endpoints/endpoints.go new file mode 100644 index 000000000..b3386aaeb --- /dev/null +++ b/cmd/kitgen/testdata/stringservice/default/endpoints/endpoints.go @@ -0,0 +1,44 @@ +package endpoints + +import "context" + +import "github.com/go-kit/kit/endpoint" + +import "github.com/go-kit/kit/cmd/kitgen/testdata/stringservice/default/service" + +type ConcatRequest struct { + A string + B string +} +type ConcatResponse struct { + S string + Err error +} + +func makeConcatEndpoint(s service.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(ConcatRequest) + string1, err := s.Concat(ctx, req.A, req.B) + return ConcatResponse{S: string1, Err: err}, nil + } +} + +type CountRequest struct { + S string +} +type CountResponse struct { + Count int +} + +func makeCountEndpoint(s service.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(CountRequest) + count := s.Count(ctx, req.S) + return CountResponse{Count: count}, nil + } +} + +type Endpoints struct { + Concat endpoint.Endpoint + Count endpoint.Endpoint +} diff --git a/cmd/kitgen/testdata/stringservice/default/http/http.go b/cmd/kitgen/testdata/stringservice/default/http/http.go new file mode 100644 index 000000000..31e2c1938 --- /dev/null +++ b/cmd/kitgen/testdata/stringservice/default/http/http.go @@ -0,0 +1,34 @@ +package http + +import "context" +import "encoding/json" + +import "net/http" + +import httptransport "github.com/go-kit/kit/transport/http" +import "github.com/go-kit/kit/cmd/kitgen/testdata/stringservice/default/endpoints" + +func NewHTTPHandler(endpoints endpoints.Endpoints) http.Handler { + m := http.NewServeMux() + m.Handle("/concat", httptransport.NewServer(endpoints.Concat, DecodeConcatRequest, EncodeConcatResponse)) + m.Handle("/count", httptransport.NewServer(endpoints.Count, DecodeCountRequest, EncodeCountResponse)) + return m +} +func DecodeConcatRequest(_ context.Context, r *http.Request) (interface{}, error) { + var req endpoints.ConcatRequest + err := json.NewDecoder(r.Body).Decode(&req) + return req, err +} +func EncodeConcatResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + return json.NewEncoder(w).Encode(response) +} +func DecodeCountRequest(_ context.Context, r *http.Request) (interface{}, error) { + var req endpoints.CountRequest + err := json.NewDecoder(r.Body).Decode(&req) + return req, err +} +func EncodeCountResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + return json.NewEncoder(w).Encode(response) +} diff --git a/cmd/kitgen/testdata/stringservice/default/service/service.go b/cmd/kitgen/testdata/stringservice/default/service/service.go new file mode 100644 index 000000000..ddf24f972 --- /dev/null +++ b/cmd/kitgen/testdata/stringservice/default/service/service.go @@ -0,0 +1,15 @@ +package service + +import "context" + +import "errors" + +type Service struct { +} + +func (s Service) Concat(ctx context.Context, a string, b string) (string, error) { + panic(errors.New("not implemented")) +} +func (s Service) Count(ctx context.Context, string1 string) int { + panic(errors.New("not implemented")) +} diff --git a/cmd/kitgen/testdata/stringservice/flat/gokit.go b/cmd/kitgen/testdata/stringservice/flat/gokit.go new file mode 100644 index 000000000..788b8b956 --- /dev/null +++ b/cmd/kitgen/testdata/stringservice/flat/gokit.go @@ -0,0 +1,80 @@ +package foo + +import "context" +import "encoding/json" +import "errors" +import "net/http" +import "github.com/go-kit/kit/endpoint" +import httptransport "github.com/go-kit/kit/transport/http" + +type Service struct { +} + +func (s Service) Concat(ctx context.Context, a string, b string) (string, error) { + panic(errors.New("not implemented")) +} + +type ConcatRequest struct { + A string + B string +} +type ConcatResponse struct { + S string + Err error +} + +func makeConcatEndpoint(s Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(ConcatRequest) + string1, err := s.Concat(ctx, req.A, req.B) + return ConcatResponse{S: string1, Err: err}, nil + } +} +func (s Service) Count(ctx context.Context, string1 string) int { + panic(errors.New("not implemented")) +} + +type CountRequest struct { + S string +} +type CountResponse struct { + Count int +} + +func makeCountEndpoint(s Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(CountRequest) + count := s.Count(ctx, req.S) + return CountResponse{Count: count}, nil + } +} + +type Endpoints struct { + Concat endpoint.Endpoint + Count endpoint.Endpoint +} + +func NewHTTPHandler(endpoints Endpoints) http.Handler { + m := http.NewServeMux() + m.Handle("/concat", httptransport.NewServer(endpoints.Concat, DecodeConcatRequest, EncodeConcatResponse)) + m.Handle("/count", httptransport.NewServer(endpoints.Count, DecodeCountRequest, EncodeCountResponse)) + return m +} +func DecodeConcatRequest(_ context.Context, r *http.Request) (interface{}, error) { + var req ConcatRequest + err := json.NewDecoder(r.Body).Decode(&req) + return req, err +} +func EncodeConcatResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + return json.NewEncoder(w).Encode(response) +} +func DecodeCountRequest(_ context.Context, r *http.Request) (interface{}, error) { + var req CountRequest + err := json.NewDecoder(r.Body).Decode(&req) + return req, err +} +func EncodeCountResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + return json.NewEncoder(w).Encode(response) +} diff --git a/cmd/kitgen/testdata/stringservice/in.go b/cmd/kitgen/testdata/stringservice/in.go new file mode 100644 index 000000000..68f018752 --- /dev/null +++ b/cmd/kitgen/testdata/stringservice/in.go @@ -0,0 +1,6 @@ +package foo + +type Service interface { + Concat(ctx context.Context, a, b string) (string, error) + Count(ctx context.Context, s string) (count int) +} diff --git a/cmd/kitgen/testdata/underscores/default/endpoints/endpoints.go b/cmd/kitgen/testdata/underscores/default/endpoints/endpoints.go new file mode 100644 index 000000000..b36f63b1c --- /dev/null +++ b/cmd/kitgen/testdata/underscores/default/endpoints/endpoints.go @@ -0,0 +1,27 @@ +package endpoints + +import "context" + +import "github.com/go-kit/kit/endpoint" + +import "github.com/go-kit/kit/cmd/kitgen/testdata/underscores/default/service" + +type FooRequest struct { + I int +} +type FooResponse struct { + I int + Err error +} + +func makeFooEndpoint(s service.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(FooRequest) + i, err := s.Foo(ctx, req.I) + return FooResponse{I: i, Err: err}, nil + } +} + +type Endpoints struct { + Foo endpoint.Endpoint +} diff --git a/cmd/kitgen/testdata/underscores/default/http/http.go b/cmd/kitgen/testdata/underscores/default/http/http.go new file mode 100644 index 000000000..a2844f048 --- /dev/null +++ b/cmd/kitgen/testdata/underscores/default/http/http.go @@ -0,0 +1,24 @@ +package http + +import "context" +import "encoding/json" + +import "net/http" + +import httptransport "github.com/go-kit/kit/transport/http" +import "github.com/go-kit/kit/cmd/kitgen/testdata/underscores/default/endpoints" + +func NewHTTPHandler(endpoints endpoints.Endpoints) http.Handler { + m := http.NewServeMux() + m.Handle("/foo", httptransport.NewServer(endpoints.Foo, DecodeFooRequest, EncodeFooResponse)) + return m +} +func DecodeFooRequest(_ context.Context, r *http.Request) (interface{}, error) { + var req endpoints.FooRequest + err := json.NewDecoder(r.Body).Decode(&req) + return req, err +} +func EncodeFooResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + return json.NewEncoder(w).Encode(response) +} diff --git a/cmd/kitgen/testdata/underscores/default/service/service.go b/cmd/kitgen/testdata/underscores/default/service/service.go new file mode 100644 index 000000000..e249a490f --- /dev/null +++ b/cmd/kitgen/testdata/underscores/default/service/service.go @@ -0,0 +1,12 @@ +package service + +import "context" + +import "errors" + +type Service struct { +} + +func (s Service) Foo(ctx context.Context, i int) (int, error) { + panic(errors.New("not implemented")) +} diff --git a/cmd/kitgen/testdata/underscores/flat/gokit.go b/cmd/kitgen/testdata/underscores/flat/gokit.go new file mode 100644 index 000000000..7f6a7da7f --- /dev/null +++ b/cmd/kitgen/testdata/underscores/flat/gokit.go @@ -0,0 +1,50 @@ +package underscores + +import "context" +import "encoding/json" +import "errors" +import "net/http" +import "github.com/go-kit/kit/endpoint" +import httptransport "github.com/go-kit/kit/transport/http" + +type Service struct { +} + +func (s Service) Foo(ctx context.Context, i int) (int, error) { + panic(errors.New("not implemented")) +} + +type FooRequest struct { + I int +} +type FooResponse struct { + I int + Err error +} + +func makeFooEndpoint(s Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(FooRequest) + i, err := s.Foo(ctx, req.I) + return FooResponse{I: i, Err: err}, nil + } +} + +type Endpoints struct { + Foo endpoint.Endpoint +} + +func NewHTTPHandler(endpoints Endpoints) http.Handler { + m := http.NewServeMux() + m.Handle("/foo", httptransport.NewServer(endpoints.Foo, DecodeFooRequest, EncodeFooResponse)) + return m +} +func DecodeFooRequest(_ context.Context, r *http.Request) (interface{}, error) { + var req FooRequest + err := json.NewDecoder(r.Body).Decode(&req) + return req, err +} +func EncodeFooResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + return json.NewEncoder(w).Encode(response) +} diff --git a/cmd/kitgen/testdata/underscores/in.go b/cmd/kitgen/testdata/underscores/in.go new file mode 100644 index 000000000..9457ee060 --- /dev/null +++ b/cmd/kitgen/testdata/underscores/in.go @@ -0,0 +1,7 @@ +package underscores + +import "context" + +type Service interface { + Foo(_ context.Context, _ int) (int, error) +} diff --git a/cmd/kitgen/transform.go b/cmd/kitgen/transform.go new file mode 100644 index 000000000..362398c92 --- /dev/null +++ b/cmd/kitgen/transform.go @@ -0,0 +1,230 @@ +package main + +import ( + "bytes" + "fmt" + "go/ast" + "go/format" + "go/token" + "io" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/davecgh/go-spew/spew" + "github.com/pkg/errors" + + "golang.org/x/tools/imports" +) + +type ( + files map[string]io.Reader + layout interface { + transformAST(ctx *sourceContext) (files, error) + } + outputTree map[string]*ast.File +) + +func (ot outputTree) addFile(path, pkgname string) *ast.File { + file := &ast.File{ + Name: id(pkgname), + Decls: []ast.Decl{}, + } + ot[path] = file + return file +} + +func getGopath() string { + gopath, set := os.LookupEnv("GOPATH") + if !set { + return filepath.Join(os.Getenv("HOME"), "go") + } + return gopath +} + +func importPath(targetDir, gopath string) (string, error) { + if !filepath.IsAbs(targetDir) { + return "", fmt.Errorf("%q is not an absolute path", targetDir) + } + + for _, dir := range filepath.SplitList(gopath) { + abspath, err := filepath.Abs(dir) + if err != nil { + continue + } + srcPath := filepath.Join(abspath, "src") + + res, err := filepath.Rel(srcPath, targetDir) + if err != nil { + continue + } + if strings.Index(res, "..") == -1 { + return res, nil + } + } + return "", fmt.Errorf("%q is not in GOPATH (%s)", targetDir, gopath) + +} + +func selectify(file *ast.File, pkgName, identName, importPath string) *ast.File { + if file.Name.Name == pkgName { + return file + } + + selector := sel(id(pkgName), id(identName)) + var did bool + if file, did = selectifyIdent(identName, file, selector); did { + addImport(file, importPath) + } + return file +} + +type selIdentFn func(ast.Node, func(ast.Node)) Visitor + +func (f selIdentFn) Visit(node ast.Node, r func(ast.Node)) Visitor { + return f(node, r) +} + +func selectifyIdent(identName string, file *ast.File, selector ast.Expr) (*ast.File, bool) { + var replaced bool + var r selIdentFn + r = selIdentFn(func(node ast.Node, replaceWith func(ast.Node)) Visitor { + switch id := node.(type) { + case *ast.SelectorExpr: + return nil + case *ast.Ident: + if id.Name == identName { + replaced = true + replaceWith(selector) + } + } + return r + }) + return WalkReplace(r, file).(*ast.File), replaced +} + +func formatNode(fname string, node ast.Node) (*bytes.Buffer, error) { + if file, is := node.(*ast.File); is { + sort.Stable(sortableDecls(file.Decls)) + } + outfset := token.NewFileSet() + buf := &bytes.Buffer{} + err := format.Node(buf, outfset, node) + if err != nil { + return nil, err + } + imps, err := imports.Process(fname, buf.Bytes(), nil) + if err != nil { + return nil, err + } + return bytes.NewBuffer(imps), nil +} + +type sortableDecls []ast.Decl + +func (sd sortableDecls) Len() int { + return len(sd) +} + +func (sd sortableDecls) Less(i int, j int) bool { + switch left := sd[i].(type) { + case *ast.GenDecl: + switch right := sd[j].(type) { + default: + return left.Tok == token.IMPORT + case *ast.GenDecl: + return left.Tok == token.IMPORT && right.Tok != token.IMPORT + } + } + return false +} + +func (sd sortableDecls) Swap(i int, j int) { + sd[i], sd[j] = sd[j], sd[i] +} + +func formatNodes(nodes outputTree) (files, error) { + res := files{} + var err error + for fn, node := range nodes { + res[fn], err = formatNode(fn, node) + if err != nil { + return nil, errors.Wrapf(err, "formatNodes") + } + } + return res, nil +} + +// XXX debug +func spewDecls(f *ast.File) { + for _, d := range f.Decls { + switch dcl := d.(type) { + default: + spew.Dump(dcl) + case *ast.GenDecl: + spew.Dump(dcl.Tok) + case *ast.FuncDecl: + spew.Dump(dcl.Name.Name) + } + } +} + +func addImports(root *ast.File, ctx *sourceContext) { + root.Decls = append(root.Decls, ctx.importDecls()...) +} + +func addImport(root *ast.File, path string) { + for _, d := range root.Decls { + if imp, is := d.(*ast.GenDecl); is && imp.Tok == token.IMPORT { + for _, s := range imp.Specs { + if s.(*ast.ImportSpec).Path.Value == `"`+path+`"` { + return // already have one + // xxx aliased imports? + } + } + } + } + root.Decls = append(root.Decls, importFor(importSpec(path))) +} + +func addStubStruct(root *ast.File, iface iface) { + root.Decls = append(root.Decls, iface.stubStructDecl()) +} + +func addType(root *ast.File, typ *ast.TypeSpec) { + root.Decls = append(root.Decls, typeDecl(typ)) +} + +func addMethod(root *ast.File, iface iface, meth method) { + def := meth.definition(iface) + root.Decls = append(root.Decls, def) +} + +func addRequestStruct(root *ast.File, meth method) { + root.Decls = append(root.Decls, meth.requestStruct()) +} + +func addResponseStruct(root *ast.File, meth method) { + root.Decls = append(root.Decls, meth.responseStruct()) +} + +func addEndpointMaker(root *ast.File, ifc iface, meth method) { + root.Decls = append(root.Decls, meth.endpointMaker(ifc)) +} + +func addEndpointsStruct(root *ast.File, ifc iface) { + root.Decls = append(root.Decls, ifc.endpointsStruct()) +} + +func addHTTPHandler(root *ast.File, ifc iface) { + root.Decls = append(root.Decls, ifc.httpHandler()) +} + +func addDecoder(root *ast.File, meth method) { + root.Decls = append(root.Decls, meth.decoderFunc()) +} + +func addEncoder(root *ast.File, meth method) { + root.Decls = append(root.Decls, meth.encoderFunc()) +} diff --git a/examples/addsvc/cmd/addcli/addcli.go b/examples/addsvc/cmd/addcli/addcli.go index e2f0f94d0..fe24fc278 100644 --- a/examples/addsvc/cmd/addcli/addcli.go +++ b/examples/addsvc/cmd/addcli/addcli.go @@ -28,7 +28,7 @@ import ( func main() { // The addcli presumes no service discovery system, and expects users to // provide the direct address of an addsvc. This presumption is reflected in - // the addcli binary and the the client packages: the -transport.addr flags + // the addcli binary and the client packages: the -transport.addr flags // and various client constructors both expect host:port strings. For an // example service with a client built on top of a service discovery system, // see profilesvc. diff --git a/examples/addsvc/cmd/addsvc/addsvc.go b/examples/addsvc/cmd/addsvc/addsvc.go index b4f8894db..b1886e2f7 100644 --- a/examples/addsvc/cmd/addsvc/addsvc.go +++ b/examples/addsvc/cmd/addsvc/addsvc.go @@ -149,8 +149,8 @@ func main() { // struct, which is a combination of 2 anonymous functions: the first // function actually runs the component, and the second function should // interrupt the first function and cause it to return. It's in these - // functions that we actually bin the Go kit server/handler structs to the - // concrete transports and start them running. + // functions that we actually bind the Go kit server/handler structs to the + // concrete transports and run them. // // Putting each component into its own block is mostly for aesthetics: it // clearly demarcates the scope in which each listener/socket may be used. diff --git a/examples/addsvc/pkg/addendpoint/set.go b/examples/addsvc/pkg/addendpoint/set.go index 3a65b083b..e4acaff47 100644 --- a/examples/addsvc/pkg/addendpoint/set.go +++ b/examples/addsvc/pkg/addendpoint/set.go @@ -2,8 +2,10 @@ package addendpoint import ( "context" + "time" + + "golang.org/x/time/rate" - rl "github.com/juju/ratelimit" stdopentracing "github.com/opentracing/opentracing-go" "github.com/sony/gobreaker" @@ -31,7 +33,7 @@ func New(svc addservice.Service, logger log.Logger, duration metrics.Histogram, var sumEndpoint endpoint.Endpoint { sumEndpoint = MakeSumEndpoint(svc) - sumEndpoint = ratelimit.NewTokenBucketLimiter(rl.NewBucketWithRate(1, 1))(sumEndpoint) + sumEndpoint = ratelimit.NewErroringLimiter(rate.NewLimiter(rate.Every(time.Second), 1))(sumEndpoint) sumEndpoint = circuitbreaker.Gobreaker(gobreaker.NewCircuitBreaker(gobreaker.Settings{}))(sumEndpoint) sumEndpoint = opentracing.TraceServer(trace, "Sum")(sumEndpoint) sumEndpoint = LoggingMiddleware(log.With(logger, "method", "Sum"))(sumEndpoint) @@ -40,7 +42,7 @@ func New(svc addservice.Service, logger log.Logger, duration metrics.Histogram, var concatEndpoint endpoint.Endpoint { concatEndpoint = MakeConcatEndpoint(svc) - concatEndpoint = ratelimit.NewTokenBucketLimiter(rl.NewBucketWithRate(100, 100))(concatEndpoint) + concatEndpoint = ratelimit.NewErroringLimiter(rate.NewLimiter(rate.Every(time.Second), 100))(concatEndpoint) concatEndpoint = circuitbreaker.Gobreaker(gobreaker.NewCircuitBreaker(gobreaker.Settings{}))(concatEndpoint) concatEndpoint = opentracing.TraceServer(trace, "Concat")(concatEndpoint) concatEndpoint = LoggingMiddleware(log.With(logger, "method", "Concat"))(concatEndpoint) diff --git a/examples/addsvc/pkg/addtransport/grpc.go b/examples/addsvc/pkg/addtransport/grpc.go index ec05baa82..6ec58d7f4 100644 --- a/examples/addsvc/pkg/addtransport/grpc.go +++ b/examples/addsvc/pkg/addtransport/grpc.go @@ -7,10 +7,10 @@ import ( "google.golang.org/grpc" - jujuratelimit "github.com/juju/ratelimit" stdopentracing "github.com/opentracing/opentracing-go" "github.com/sony/gobreaker" oldcontext "golang.org/x/net/context" + "golang.org/x/time/rate" "github.com/go-kit/kit/circuitbreaker" "github.com/go-kit/kit/endpoint" @@ -76,7 +76,7 @@ func NewGRPCClient(conn *grpc.ClientConn, tracer stdopentracing.Tracer, logger l // construct per-endpoint circuitbreaker middlewares to demonstrate how // that's done, although they could easily be combined into a single breaker // for the entire remote instance, too. - limiter := ratelimit.NewTokenBucketLimiter(jujuratelimit.NewBucketWithRate(100, 100)) + limiter := ratelimit.NewErroringLimiter(rate.NewLimiter(rate.Every(time.Second), 100)) // Each individual endpoint is an http/transport.Client (which implements // endpoint.Endpoint) that gets wrapped with various middlewares. If you diff --git a/examples/addsvc/pkg/addtransport/http.go b/examples/addsvc/pkg/addtransport/http.go index ecdee9288..3819c6d87 100644 --- a/examples/addsvc/pkg/addtransport/http.go +++ b/examples/addsvc/pkg/addtransport/http.go @@ -11,7 +11,8 @@ import ( "strings" "time" - jujuratelimit "github.com/juju/ratelimit" + "golang.org/x/time/rate" + stdopentracing "github.com/opentracing/opentracing-go" "github.com/sony/gobreaker" @@ -68,7 +69,7 @@ func NewHTTPClient(instance string, tracer stdopentracing.Tracer, logger log.Log // construct per-endpoint circuitbreaker middlewares to demonstrate how // that's done, although they could easily be combined into a single breaker // for the entire remote instance, too. - limiter := ratelimit.NewTokenBucketLimiter(jujuratelimit.NewBucketWithRate(100, 100)) + limiter := ratelimit.NewErroringLimiter(rate.NewLimiter(rate.Every(time.Second), 100)) // Each individual endpoint is an http/transport.Client (which implements // endpoint.Endpoint) that gets wrapped with various middlewares. If you diff --git a/examples/addsvc/pkg/addtransport/thrift.go b/examples/addsvc/pkg/addtransport/thrift.go index c6797ecbd..485840fe0 100644 --- a/examples/addsvc/pkg/addtransport/thrift.go +++ b/examples/addsvc/pkg/addtransport/thrift.go @@ -4,7 +4,8 @@ import ( "context" "time" - jujuratelimit "github.com/juju/ratelimit" + "golang.org/x/time/rate" + "github.com/sony/gobreaker" "github.com/go-kit/kit/circuitbreaker" @@ -58,7 +59,7 @@ func NewThriftClient(client *addthrift.AddServiceClient) addservice.Service { // construct per-endpoint circuitbreaker middlewares to demonstrate how // that's done, although they could easily be combined into a single breaker // for the entire remote instance, too. - limiter := ratelimit.NewTokenBucketLimiter(jujuratelimit.NewBucketWithRate(100, 100)) + limiter := ratelimit.NewErroringLimiter(rate.NewLimiter(rate.Every(time.Second), 100)) // Each individual endpoint is an http/transport.Client (which implements // endpoint.Endpoint) that gets wrapped with various middlewares. If you diff --git a/examples/addsvc/thrift/gen-go/addsvc/add_service-remote/add_service-remote.go b/examples/addsvc/thrift/gen-go/addsvc/add_service-remote/add_service-remote.go index 2063763a9..8ef18eafd 100755 --- a/examples/addsvc/thrift/gen-go/addsvc/add_service-remote/add_service-remote.go +++ b/examples/addsvc/thrift/gen-go/addsvc/add_service-remote/add_service-remote.go @@ -111,7 +111,9 @@ func main() { Usage() os.Exit(1) } - client := addsvc.NewAddServiceClientFactory(trans, protocolFactory) + iprot := protocolFactory.GetProtocol(trans) + oprot := protocolFactory.GetProtocol(trans) + client := addsvc.NewAddServiceClient(thrift.NewTStandardClient(iprot, oprot)) if err := trans.Open(); err != nil { fmt.Fprintln(os.Stderr, "Error opening socket to ", host, ":", port, " ", err) os.Exit(1) diff --git a/examples/addsvc/thrift/gen-go/addsvc/addsvc.go b/examples/addsvc/thrift/gen-go/addsvc/addsvc.go index 4f695b93a..729ad6226 100644 --- a/examples/addsvc/thrift/gen-go/addsvc/addsvc.go +++ b/examples/addsvc/thrift/gen-go/addsvc/addsvc.go @@ -284,28 +284,26 @@ type AddService interface { } type AddServiceClient struct { - Transport thrift.TTransport - ProtocolFactory thrift.TProtocolFactory - InputProtocol thrift.TProtocol - OutputProtocol thrift.TProtocol - SeqId int32 + c thrift.TClient } +// Deprecated: Use NewAddService instead func NewAddServiceClientFactory(t thrift.TTransport, f thrift.TProtocolFactory) *AddServiceClient { - return &AddServiceClient{Transport: t, - ProtocolFactory: f, - InputProtocol: f.GetProtocol(t), - OutputProtocol: f.GetProtocol(t), - SeqId: 0, + return &AddServiceClient{ + c: thrift.NewTStandardClient(f.GetProtocol(t), f.GetProtocol(t)), } } +// Deprecated: Use NewAddService instead func NewAddServiceClientProtocol(t thrift.TTransport, iprot thrift.TProtocol, oprot thrift.TProtocol) *AddServiceClient { - return &AddServiceClient{Transport: t, - ProtocolFactory: nil, - InputProtocol: iprot, - OutputProtocol: oprot, - SeqId: 0, + return &AddServiceClient{ + c: thrift.NewTStandardClient(iprot, oprot), + } +} + +func NewAddServiceClient(c thrift.TClient) *AddServiceClient { + return &AddServiceClient{ + c: c, } } @@ -313,159 +311,30 @@ func NewAddServiceClientProtocol(t thrift.TTransport, iprot thrift.TProtocol, op // - A // - B func (p *AddServiceClient) Sum(ctx context.Context, a int64, b int64) (r *SumReply, err error) { - if err = p.sendSum(a, b); err != nil { return } - return p.recvSum() -} - -func (p *AddServiceClient) sendSum(a int64, b int64)(err error) { - oprot := p.OutputProtocol - if oprot == nil { - oprot = p.ProtocolFactory.GetProtocol(p.Transport) - p.OutputProtocol = oprot - } - p.SeqId++ - if err = oprot.WriteMessageBegin("Sum", thrift.CALL, p.SeqId); err != nil { - return - } - args := AddServiceSumArgs{ - A : a, - B : b, - } - if err = args.Write(oprot); err != nil { - return - } - if err = oprot.WriteMessageEnd(); err != nil { - return - } - return oprot.Flush() -} - - -func (p *AddServiceClient) recvSum() (value *SumReply, err error) { - iprot := p.InputProtocol - if iprot == nil { - iprot = p.ProtocolFactory.GetProtocol(p.Transport) - p.InputProtocol = iprot - } - method, mTypeId, seqId, err := iprot.ReadMessageBegin() - if err != nil { - return - } - if method != "Sum" { - err = thrift.NewTApplicationException(thrift.WRONG_METHOD_NAME, "Sum failed: wrong method name") - return - } - if p.SeqId != seqId { - err = thrift.NewTApplicationException(thrift.BAD_SEQUENCE_ID, "Sum failed: out of sequence response") - return - } - if mTypeId == thrift.EXCEPTION { - error0 := thrift.NewTApplicationException(thrift.UNKNOWN_APPLICATION_EXCEPTION, "Unknown Exception") - var error1 error - error1, err = error0.Read(iprot) - if err != nil { - return - } - if err = iprot.ReadMessageEnd(); err != nil { - return - } - err = error1 - return - } - if mTypeId != thrift.REPLY { - err = thrift.NewTApplicationException(thrift.INVALID_MESSAGE_TYPE_EXCEPTION, "Sum failed: invalid message type") - return - } - result := AddServiceSumResult{} - if err = result.Read(iprot); err != nil { - return - } - if err = iprot.ReadMessageEnd(); err != nil { + var _args0 AddServiceSumArgs + _args0.A = a + _args0.B = b + var _result1 AddServiceSumResult + if err = p.c.Call(ctx, "Sum", &_args0, &_result1); err != nil { return } - value = result.GetSuccess() - return + return _result1.GetSuccess(), nil } // Parameters: // - A // - B func (p *AddServiceClient) Concat(ctx context.Context, a string, b string) (r *ConcatReply, err error) { - if err = p.sendConcat(a, b); err != nil { return } - return p.recvConcat() -} - -func (p *AddServiceClient) sendConcat(a string, b string)(err error) { - oprot := p.OutputProtocol - if oprot == nil { - oprot = p.ProtocolFactory.GetProtocol(p.Transport) - p.OutputProtocol = oprot - } - p.SeqId++ - if err = oprot.WriteMessageBegin("Concat", thrift.CALL, p.SeqId); err != nil { - return - } - args := AddServiceConcatArgs{ - A : a, - B : b, - } - if err = args.Write(oprot); err != nil { - return - } - if err = oprot.WriteMessageEnd(); err != nil { - return - } - return oprot.Flush() -} - - -func (p *AddServiceClient) recvConcat() (value *ConcatReply, err error) { - iprot := p.InputProtocol - if iprot == nil { - iprot = p.ProtocolFactory.GetProtocol(p.Transport) - p.InputProtocol = iprot - } - method, mTypeId, seqId, err := iprot.ReadMessageBegin() - if err != nil { - return - } - if method != "Concat" { - err = thrift.NewTApplicationException(thrift.WRONG_METHOD_NAME, "Concat failed: wrong method name") + var _args2 AddServiceConcatArgs + _args2.A = a + _args2.B = b + var _result3 AddServiceConcatResult + if err = p.c.Call(ctx, "Concat", &_args2, &_result3); err != nil { return } - if p.SeqId != seqId { - err = thrift.NewTApplicationException(thrift.BAD_SEQUENCE_ID, "Concat failed: out of sequence response") - return - } - if mTypeId == thrift.EXCEPTION { - error2 := thrift.NewTApplicationException(thrift.UNKNOWN_APPLICATION_EXCEPTION, "Unknown Exception") - var error3 error - error3, err = error2.Read(iprot) - if err != nil { - return - } - if err = iprot.ReadMessageEnd(); err != nil { - return - } - err = error3 - return - } - if mTypeId != thrift.REPLY { - err = thrift.NewTApplicationException(thrift.INVALID_MESSAGE_TYPE_EXCEPTION, "Concat failed: invalid message type") - return - } - result := AddServiceConcatResult{} - if err = result.Read(iprot); err != nil { - return - } - if err = iprot.ReadMessageEnd(); err != nil { - return - } - value = result.GetSuccess() - return + return _result3.GetSuccess(), nil } - type AddServiceProcessor struct { processorMap map[string]thrift.TProcessorFunction handler AddService diff --git a/examples/stringsvc3/proxying.go b/examples/stringsvc3/proxying.go index 8b1013f31..0f6780776 100644 --- a/examples/stringsvc3/proxying.go +++ b/examples/stringsvc3/proxying.go @@ -8,7 +8,8 @@ import ( "strings" "time" - jujuratelimit "github.com/juju/ratelimit" + "golang.org/x/time/rate" + "github.com/sony/gobreaker" "github.com/go-kit/kit/circuitbreaker" @@ -47,7 +48,7 @@ func proxyingMiddleware(ctx context.Context, instances string, logger log.Logger var e endpoint.Endpoint e = makeUppercaseProxy(ctx, instance) e = circuitbreaker.Gobreaker(gobreaker.NewCircuitBreaker(gobreaker.Settings{}))(e) - e = ratelimit.NewTokenBucketLimiter(jujuratelimit.NewBucketWithRate(float64(qps), int64(qps)))(e) + e = ratelimit.NewErroringLimiter(rate.NewLimiter(rate.Every(time.Second), qps))(e) endpointer = append(endpointer, e) } diff --git a/metrics/cloudwatch/cloudwatch.go b/metrics/cloudwatch/cloudwatch.go index e267e0302..4322d4cf2 100644 --- a/metrics/cloudwatch/cloudwatch.go +++ b/metrics/cloudwatch/cloudwatch.go @@ -2,6 +2,7 @@ package cloudwatch import ( "fmt" + "os" "sync" "time" @@ -12,12 +13,19 @@ import ( "github.com/go-kit/kit/log" "github.com/go-kit/kit/metrics" "github.com/go-kit/kit/metrics/generic" + "github.com/go-kit/kit/metrics/internal/lv" + "strconv" ) const ( maxConcurrentRequests = 20 ) +type Percentiles []struct { + s string + f float64 +} + // CloudWatch receives metrics observations and forwards them to CloudWatch. // Create a CloudWatch object, use it to create metrics, and pass those metrics as // dependencies to the components that will use them. @@ -27,66 +35,104 @@ type CloudWatch struct { mtx sync.RWMutex sem chan struct{} namespace string - numConcurrentRequests int svc cloudwatchiface.CloudWatchAPI - counters map[string]*counter - gauges map[string]*gauge - histograms map[string]*histogram + counters *lv.Space + gauges *lv.Space + histograms *lv.Space + percentiles []float64 // percentiles to track logger log.Logger + numConcurrentRequests int +} + +type option func(*CloudWatch) + +func (s *CloudWatch) apply(opt option) { + if opt != nil { + opt(s) + } +} + +func WithLogger(logger log.Logger) option { + return func(c *CloudWatch) { + c.logger = logger + } +} + +// WithPercentiles registers the percentiles to track, overriding the +// existing/default values. +// Reason is that Cloudwatch makes you pay per metric, so you can save half the money +// by only using 2 metrics instead of the default 4. +func WithPercentiles(percentiles ...float64) option { + return func(c *CloudWatch) { + c.percentiles = make([]float64, 0, len(percentiles)) + for _, p := range percentiles { + if p < 0 || p > 1 { + continue // illegal entry; ignore + } + c.percentiles = append(c.percentiles, p) + } + } +} + +func WithConcurrentRequests(n int) option { + return func(c *CloudWatch) { + if n > maxConcurrentRequests { + n = maxConcurrentRequests + } + c.numConcurrentRequests = n + } } // New returns a CloudWatch object that may be used to create metrics. // Namespace is applied to all created metrics and maps to the CloudWatch namespace. -// NumConcurrent sets the number of simultaneous requests to Amazon. -// A good default value is 10 and the maximum is 20. // Callers must ensure that regular calls to Send are performed, either // manually or with one of the helper methods. -func New(namespace string, svc cloudwatchiface.CloudWatchAPI, numConcurrent int, logger log.Logger) *CloudWatch { - if numConcurrent > maxConcurrentRequests { - numConcurrent = maxConcurrentRequests +func New(namespace string, svc cloudwatchiface.CloudWatchAPI, options ...option) *CloudWatch { + cw := &CloudWatch{ + sem: nil, // set below + namespace: namespace, + svc: svc, + counters: lv.NewSpace(), + gauges: lv.NewSpace(), + histograms: lv.NewSpace(), + numConcurrentRequests: 10, + logger: log.NewLogfmtLogger(os.Stderr), + percentiles: []float64{0.50, 0.90, 0.95, 0.99}, } - return &CloudWatch{ - sem: make(chan struct{}, numConcurrent), - namespace: namespace, - numConcurrentRequests: numConcurrent, - svc: svc, - counters: map[string]*counter{}, - gauges: map[string]*gauge{}, - histograms: map[string]*histogram{}, - logger: logger, + for _, optFunc := range options { + optFunc(cw) } + + cw.sem = make(chan struct{}, cw.numConcurrentRequests) + + return cw } // NewCounter returns a counter. Observations are aggregated and emitted once // per write invocation. func (cw *CloudWatch) NewCounter(name string) metrics.Counter { - cw.mtx.Lock() - defer cw.mtx.Unlock() - c := &counter{c: generic.NewCounter(name)} - cw.counters[name] = c - return c + return &Counter{ + name: name, + obs: cw.counters.Observe, + } } -// NewGauge returns a gauge. Observations are aggregated and emitted once per -// write invocation. +// NewGauge returns an gauge. func (cw *CloudWatch) NewGauge(name string) metrics.Gauge { - cw.mtx.Lock() - defer cw.mtx.Unlock() - g := &gauge{g: generic.NewGauge(name)} - cw.gauges[name] = g - return g + return &Gauge{ + name: name, + obs: cw.gauges.Observe, + add: cw.gauges.Add, + } } -// NewHistogram returns a histogram. Observations are aggregated and emitted as -// per-quantile gauges, once per write invocation. 50 is a good default value -// for buckets. -func (cw *CloudWatch) NewHistogram(name string, buckets int) metrics.Histogram { - cw.mtx.Lock() - defer cw.mtx.Unlock() - h := &histogram{h: generic.NewHistogram(name, buckets)} - cw.histograms[name] = h - return h +// NewHistogram returns a histogram. +func (cw *CloudWatch) NewHistogram(name string) metrics.Histogram { + return &Histogram{ + name: name, + obs: cw.histograms.Observe, + } } // WriteLoop is a helper method that invokes Send every time the passed @@ -110,42 +156,54 @@ func (cw *CloudWatch) Send() error { var datums []*cloudwatch.MetricDatum - for name, c := range cw.counters { + cw.counters.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool { + value := sum(values) datums = append(datums, &cloudwatch.MetricDatum{ MetricName: aws.String(name), - Dimensions: makeDimensions(c.c.LabelValues()...), - Value: aws.Float64(c.c.Value()), + Dimensions: makeDimensions(lvs...), + Value: aws.Float64(value), Timestamp: aws.Time(now), }) - } + return true + }) - for name, g := range cw.gauges { + cw.gauges.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool { + value := last(values) datums = append(datums, &cloudwatch.MetricDatum{ MetricName: aws.String(name), - Dimensions: makeDimensions(g.g.LabelValues()...), - Value: aws.Float64(g.g.Value()), + Dimensions: makeDimensions(lvs...), + Value: aws.Float64(value), Timestamp: aws.Time(now), }) + return true + }) + + // format a [0,1]-float value to a percentile value, with minimum nr of decimals + // 0.90 -> "90" + // 0.95 -> "95" + // 0.999 -> "99.9" + formatPerc := func(p float64) string { + return strconv.FormatFloat(p*100, 'f', -1, 64) } - for name, h := range cw.histograms { - for _, p := range []struct { - s string - f float64 - }{ - {"50", 0.50}, - {"90", 0.90}, - {"95", 0.95}, - {"99", 0.99}, - } { + cw.histograms.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool { + histogram := generic.NewHistogram(name, 50) + + for _, v := range values { + histogram.Observe(v) + } + + for _, perc := range cw.percentiles { + value := histogram.Quantile(perc) datums = append(datums, &cloudwatch.MetricDatum{ - MetricName: aws.String(fmt.Sprintf("%s_%s", name, p.s)), - Dimensions: makeDimensions(h.h.LabelValues()...), - Value: aws.Float64(h.h.Quantile(p.f)), + MetricName: aws.String(fmt.Sprintf("%s_%s", name, formatPerc(perc))), + Dimensions: makeDimensions(lvs...), + Value: aws.Float64(value), Timestamp: aws.Time(now), }) } - } + return true + }) var batches [][]*cloudwatch.MetricDatum for len(datums) > 0 { @@ -179,6 +237,18 @@ func (cw *CloudWatch) Send() error { return firstErr } +func sum(a []float64) float64 { + var v float64 + for _, f := range a { + v += f + } + return v +} + +func last(a []float64) float64 { + return a[len(a)-1] +} + func min(a, b int) int { if a < b { return a @@ -186,57 +256,79 @@ func min(a, b int) int { return b } -// counter is a CloudWatch counter metric. -type counter struct { - c *generic.Counter +type observeFunc func(name string, lvs lv.LabelValues, value float64) + +// Counter is a counter. Observations are forwarded to a node +// object, and aggregated (summed) per timeseries. +type Counter struct { + name string + lvs lv.LabelValues + obs observeFunc } -// With implements counter -func (c *counter) With(labelValues ...string) metrics.Counter { - c.c = c.c.With(labelValues...).(*generic.Counter) - return c +// With implements metrics.Counter. +func (c *Counter) With(labelValues ...string) metrics.Counter { + return &Counter{ + name: c.name, + lvs: c.lvs.With(labelValues...), + obs: c.obs, + } } -// Add implements counter. -func (c *counter) Add(delta float64) { - c.c.Add(delta) +// Add implements metrics.Counter. +func (c *Counter) Add(delta float64) { + c.obs(c.name, c.lvs, delta) } -// gauge is a CloudWatch gauge metric. -type gauge struct { - g *generic.Gauge +// Gauge is a gauge. Observations are forwarded to a node +// object, and aggregated (the last observation selected) per timeseries. +type Gauge struct { + name string + lvs lv.LabelValues + obs observeFunc + add observeFunc } -// With implements gauge -func (g *gauge) With(labelValues ...string) metrics.Gauge { - g.g = g.g.With(labelValues...).(*generic.Gauge) - return g +// With implements metrics.Gauge. +func (g *Gauge) With(labelValues ...string) metrics.Gauge { + return &Gauge{ + name: g.name, + lvs: g.lvs.With(labelValues...), + obs: g.obs, + add: g.add, + } } -// Set implements gauge -func (g *gauge) Set(value float64) { - g.g.Set(value) +// Set implements metrics.Gauge. +func (g *Gauge) Set(value float64) { + g.obs(g.name, g.lvs, value) } -// Add implements gauge -func (g *gauge) Add(delta float64) { - g.g.Add(delta) +// Add implements metrics.Gauge. +func (g *Gauge) Add(delta float64) { + g.add(g.name, g.lvs, delta) } -// histogram is a CloudWatch histogram metric -type histogram struct { - h *generic.Histogram +// Histogram is an Influx histrogram. Observations are aggregated into a +// generic.Histogram and emitted as per-quantile gauges to the Influx server. +type Histogram struct { + name string + lvs lv.LabelValues + obs observeFunc } -// With implements histogram -func (h *histogram) With(labelValues ...string) metrics.Histogram { - h.h = h.h.With(labelValues...).(*generic.Histogram) - return h +// With implements metrics.Histogram. +func (h *Histogram) With(labelValues ...string) metrics.Histogram { + return &Histogram{ + name: h.name, + lvs: h.lvs.With(labelValues...), + obs: h.obs, + } } -// Observe implements histogram -func (h *histogram) Observe(value float64) { - h.h.Observe(value) +// Observe implements metrics.Histogram. +func (h *Histogram) Observe(value float64) { + h.obs(h.name, h.lvs, value) } func makeDimensions(labelValues ...string) []*cloudwatch.Dimension { diff --git a/metrics/cloudwatch/cloudwatch_test.go b/metrics/cloudwatch/cloudwatch_test.go index d36d9b2aa..e5442cbc0 100644 --- a/metrics/cloudwatch/cloudwatch_test.go +++ b/metrics/cloudwatch/cloudwatch_test.go @@ -39,8 +39,15 @@ func (mcw *mockCloudWatch) PutMetricData(input *cloudwatch.PutMetricDataInput) ( return nil, nil } -func testDimensions(svc *mockCloudWatch, name string, labelValues ...string) error { - dimensions, ok := svc.dimensionsReceived[name] +func (mcw *mockCloudWatch) testDimensions(name string, labelValues ...string) error { + mcw.mtx.RLock() + _, hasValue := mcw.valuesReceived[name] + if !hasValue { + return nil // nothing to check; 0 samples were received + } + dimensions, ok := mcw.dimensionsReceived[name] + mcw.mtx.RUnlock() + if !ok { if len(labelValues) > 0 { return errors.New("Expected dimensions to be available, but none were") @@ -66,7 +73,7 @@ func TestCounter(t *testing.T) { namespace, name := "abc", "def" label, value := "label", "value" svc := newMockCloudWatch() - cw := New(namespace, svc, 10, log.NewNopLogger()) + cw := New(namespace, svc, WithLogger(log.NewNopLogger())) counter := cw.NewCounter(name).With(label, value) valuef := func() float64 { err := cw.Send() @@ -80,7 +87,10 @@ func TestCounter(t *testing.T) { if err := teststat.TestCounter(counter, valuef); err != nil { t.Fatal(err) } - if err := testDimensions(svc, name, label, value); err != nil { + if err := teststat.TestCounter(counter, valuef); err != nil { + t.Fatal("Fill and flush counter 2nd time: ", err) + } + if err := svc.testDimensions(name, label, value); err != nil { t.Fatal(err) } } @@ -95,7 +105,10 @@ func TestCounterLowSendConcurrency(t *testing.T) { values = append(values, "value"+num) } svc := newMockCloudWatch() - cw := New(namespace, svc, 2, log.NewNopLogger()) + cw := New(namespace, svc, + WithLogger(log.NewNopLogger()), + WithConcurrentRequests(2), + ) counters := make(map[string]metrics.Counter) var wants []float64 @@ -113,7 +126,7 @@ func TestCounterLowSendConcurrency(t *testing.T) { if svc.valuesReceived[name] != wants[i] { t.Fatalf("want %f, have %f", wants[i], svc.valuesReceived[name]) } - if err := testDimensions(svc, name, labels[i], values[i]); err != nil { + if err := svc.testDimensions(name, labels[i], values[i]); err != nil { t.Fatal(err) } } @@ -123,7 +136,7 @@ func TestGauge(t *testing.T) { namespace, name := "abc", "def" label, value := "label", "value" svc := newMockCloudWatch() - cw := New(namespace, svc, 10, log.NewNopLogger()) + cw := New(namespace, svc, WithLogger(log.NewNopLogger())) gauge := cw.NewGauge(name).With(label, value) valuef := func() float64 { err := cw.Send() @@ -137,7 +150,7 @@ func TestGauge(t *testing.T) { if err := teststat.TestGauge(gauge, valuef); err != nil { t.Fatal(err) } - if err := testDimensions(svc, name, label, value); err != nil { + if err := svc.testDimensions(name, label, value); err != nil { t.Fatal(err) } } @@ -146,8 +159,8 @@ func TestHistogram(t *testing.T) { namespace, name := "abc", "def" label, value := "label", "value" svc := newMockCloudWatch() - cw := New(namespace, svc, 10, log.NewNopLogger()) - histogram := cw.NewHistogram(name, 50).With(label, value) + cw := New(namespace, svc, WithLogger(log.NewNopLogger())) + histogram := cw.NewHistogram(name).With(label, value) n50 := fmt.Sprintf("%s_50", name) n90 := fmt.Sprintf("%s_90", name) n95 := fmt.Sprintf("%s_95", name) @@ -168,16 +181,64 @@ func TestHistogram(t *testing.T) { if err := teststat.TestHistogram(histogram, quantiles, 0.01); err != nil { t.Fatal(err) } - if err := testDimensions(svc, n50, label, value); err != nil { + if err := svc.testDimensions(n50, label, value); err != nil { + t.Fatal(err) + } + if err := svc.testDimensions(n90, label, value); err != nil { + t.Fatal(err) + } + if err := svc.testDimensions(n95, label, value); err != nil { + t.Fatal(err) + } + if err := svc.testDimensions(n99, label, value); err != nil { + t.Fatal(err) + } + + // now test with only 2 custom percentiles + // + svc = newMockCloudWatch() + cw = New(namespace, svc, WithLogger(log.NewNopLogger()), WithPercentiles(0.50, 0.90)) + histogram = cw.NewHistogram(name).With(label, value) + + customQuantiles := func() (p50, p90, p95, p99 float64) { + err := cw.Send() + if err != nil { + t.Fatal(err) + } + svc.mtx.RLock() + defer svc.mtx.RUnlock() + p50 = svc.valuesReceived[n50] + p90 = svc.valuesReceived[n90] + + // our teststat.TestHistogram wants us to give p95 and p99, + // but with custom percentiles we don't have those. + // So fake them. Maybe we should make teststat.nvq() public and use that? + p95 = 541.121341 + p99 = 558.158697 + + // but fail if they are actually set (because that would mean the + // WithPercentiles() is not respected) + if _, isSet := svc.valuesReceived[n95]; isSet { + t.Fatal("p95 should not be set") + } + if _, isSet := svc.valuesReceived[n99]; isSet { + t.Fatal("p99 should not be set") + } + return + } + if err := teststat.TestHistogram(histogram, customQuantiles, 0.01); err != nil { + t.Fatal(err) + } + if err := svc.testDimensions(n50, label, value); err != nil { t.Fatal(err) } - if err := testDimensions(svc, n90, label, value); err != nil { + if err := svc.testDimensions(n90, label, value); err != nil { t.Fatal(err) } - if err := testDimensions(svc, n95, label, value); err != nil { + if err := svc.testDimensions(n95, label, value); err != nil { t.Fatal(err) } - if err := testDimensions(svc, n99, label, value); err != nil { + if err := svc.testDimensions(n99, label, value); err != nil { t.Fatal(err) } } diff --git a/metrics/debug.test b/metrics/debug.test new file mode 100755 index 000000000..f9a180b6d Binary files /dev/null and b/metrics/debug.test differ diff --git a/metrics/doc.go b/metrics/doc.go index 9be8e3017..25cda4f7c 100644 --- a/metrics/doc.go +++ b/metrics/doc.go @@ -48,7 +48,7 @@ // Namespace: "myteam", // Subsystem: "foosvc", // Name: "request_latency_seconds", -// Help: "Incoming request latency in seconds." +// Help: "Incoming request latency in seconds.", // }, []string{"method", "status_code"}) // // Write your components to take the metrics they will use as parameters to diff --git a/metrics/dogstatsd/dogstatsd.go b/metrics/dogstatsd/dogstatsd.go index 13e0b4f54..ccdcd57b4 100644 --- a/metrics/dogstatsd/dogstatsd.go +++ b/metrics/dogstatsd/dogstatsd.go @@ -14,10 +14,13 @@ import ( "fmt" "io" "strings" + "sync" + "sync/atomic" "time" "github.com/go-kit/kit/log" "github.com/go-kit/kit/metrics" + "github.com/go-kit/kit/metrics/generic" "github.com/go-kit/kit/metrics/internal/lv" "github.com/go-kit/kit/metrics/internal/ratemap" "github.com/go-kit/kit/util/conn" @@ -34,54 +37,63 @@ import ( // To regularly report metrics to an io.Writer, use the WriteLoop helper method. // To send to a DogStatsD server, use the SendLoop helper method. type Dogstatsd struct { + mtx sync.RWMutex prefix string rates *ratemap.RateMap counters *lv.Space - gauges *lv.Space + gauges map[string]*gaugeNode timings *lv.Space histograms *lv.Space logger log.Logger + lvs lv.LabelValues } // New returns a Dogstatsd object that may be used to create metrics. Prefix is // applied to all created metrics. Callers must ensure that regular calls to // WriteTo are performed, either manually or with one of the helper methods. -func New(prefix string, logger log.Logger) *Dogstatsd { +func New(prefix string, logger log.Logger, lvs ...string) *Dogstatsd { + if len(lvs)%2 != 0 { + panic("odd number of LabelValues; programmer error!") + } return &Dogstatsd{ prefix: prefix, rates: ratemap.New(), counters: lv.NewSpace(), - gauges: lv.NewSpace(), + gauges: map[string]*gaugeNode{}, timings: lv.NewSpace(), histograms: lv.NewSpace(), logger: logger, + lvs: lvs, } } // NewCounter returns a counter, sending observations to this Dogstatsd object. func (d *Dogstatsd) NewCounter(name string, sampleRate float64) *Counter { - d.rates.Set(d.prefix+name, sampleRate) + d.rates.Set(name, sampleRate) return &Counter{ - name: d.prefix + name, + name: name, obs: d.counters.Observe, } } // NewGauge returns a gauge, sending observations to this Dogstatsd object. func (d *Dogstatsd) NewGauge(name string) *Gauge { - return &Gauge{ - name: d.prefix + name, - obs: d.gauges.Observe, - add: d.gauges.Add, + d.mtx.Lock() + n, ok := d.gauges[name] + if !ok { + n = &gaugeNode{gauge: &Gauge{g: generic.NewGauge(name), ddog: d}} + d.gauges[name] = n } + d.mtx.Unlock() + return n.gauge } // NewTiming returns a histogram whose observations are interpreted as // millisecond durations, and are forwarded to this Dogstatsd object. func (d *Dogstatsd) NewTiming(name string, sampleRate float64) *Timing { - d.rates.Set(d.prefix+name, sampleRate) + d.rates.Set(name, sampleRate) return &Timing{ - name: d.prefix + name, + name: name, obs: d.timings.Observe, } } @@ -89,9 +101,9 @@ func (d *Dogstatsd) NewTiming(name string, sampleRate float64) *Timing { // NewHistogram returns a histogram whose observations are of an unspecified // unit, and are forwarded to this Dogstatsd object. func (d *Dogstatsd) NewHistogram(name string, sampleRate float64) *Histogram { - d.rates.Set(d.prefix+name, sampleRate) + d.rates.Set(name, sampleRate) return &Histogram{ - name: d.prefix + name, + name: name, obs: d.histograms.Observe, } } @@ -125,7 +137,7 @@ func (d *Dogstatsd) WriteTo(w io.Writer) (count int64, err error) { var n int d.counters.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool { - n, err = fmt.Fprintf(w, "%s:%f|c%s%s\n", name, sum(values), sampling(d.rates.Get(name)), tagValues(lvs)) + n, err = fmt.Fprintf(w, "%s%s:%f|c%s%s\n", d.prefix, name, sum(values), sampling(d.rates.Get(name)), d.tagValues(lvs)) if err != nil { return false } @@ -136,22 +148,23 @@ func (d *Dogstatsd) WriteTo(w io.Writer) (count int64, err error) { return count, err } - d.gauges.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool { - n, err = fmt.Fprintf(w, "%s:%f|g%s\n", name, last(values), tagValues(lvs)) - if err != nil { - return false - } - count += int64(n) - return true - }) - if err != nil { - return count, err + d.mtx.RLock() + for _, root := range d.gauges { + root.walk(func(name string, lvs lv.LabelValues, value float64) bool { + n, err = fmt.Fprintf(w, "%s%s:%f|g%s\n", d.prefix, name, value, d.tagValues(lvs)) + if err != nil { + return false + } + count += int64(n) + return true + }) } + d.mtx.RUnlock() d.timings.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool { sampleRate := d.rates.Get(name) for _, value := range values { - n, err = fmt.Fprintf(w, "%s:%f|ms%s%s\n", name, value, sampling(sampleRate), tagValues(lvs)) + n, err = fmt.Fprintf(w, "%s%s:%f|ms%s%s\n", d.prefix, name, value, sampling(sampleRate), d.tagValues(lvs)) if err != nil { return false } @@ -166,7 +179,7 @@ func (d *Dogstatsd) WriteTo(w io.Writer) (count int64, err error) { d.histograms.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool { sampleRate := d.rates.Get(name) for _, value := range values { - n, err = fmt.Fprintf(w, "%s:%f|h%s%s\n", name, value, sampling(sampleRate), tagValues(lvs)) + n, err = fmt.Fprintf(w, "%s%s:%f|h%s%s\n", d.prefix, name, value, sampling(sampleRate), d.tagValues(lvs)) if err != nil { return false } @@ -201,14 +214,17 @@ func sampling(r float64) string { return sv } -func tagValues(labelValues []string) string { - if len(labelValues) == 0 { +func (d *Dogstatsd) tagValues(labelValues []string) string { + if len(labelValues) == 0 && len(d.lvs) == 0 { return "" } if len(labelValues)%2 != 0 { panic("tagValues received a labelValues with an odd number of strings") } - pairs := make([]string, 0, len(labelValues)/2) + pairs := make([]string, 0, (len(d.lvs)+len(labelValues))/2) + for i := 0; i < len(d.lvs); i += 2 { + pairs = append(pairs, d.lvs[i]+":"+d.lvs[i+1]) + } for i := 0; i < len(labelValues); i += 2 { pairs = append(pairs, labelValues[i]+":"+labelValues[i+1]) } @@ -242,30 +258,31 @@ func (c *Counter) Add(delta float64) { // Gauge is a DogStatsD gauge. Observations are forwarded to a Dogstatsd // object, and aggregated (the last observation selected) per timeseries. type Gauge struct { - name string - lvs lv.LabelValues - obs observeFunc - add observeFunc + g *generic.Gauge + ddog *Dogstatsd + set int32 } // With implements metrics.Gauge. func (g *Gauge) With(labelValues ...string) metrics.Gauge { - return &Gauge{ - name: g.name, - lvs: g.lvs.With(labelValues...), - obs: g.obs, - add: g.add, - } + g.ddog.mtx.RLock() + node := g.ddog.gauges[g.g.Name] + g.ddog.mtx.RUnlock() + + ga := &Gauge{g: g.g.With(labelValues...).(*generic.Gauge), ddog: g.ddog} + return node.addGauge(ga, ga.g.LabelValues()) } // Set implements metrics.Gauge. func (g *Gauge) Set(value float64) { - g.obs(g.name, g.lvs, value) + g.g.Set(value) + g.touch() } // Add implements metrics.Gauge. func (g *Gauge) Add(delta float64) { - g.add(g.name, g.lvs, delta) + g.g.Add(delta) + g.touch() } // Timing is a DogStatsD timing, or metrics.Histogram. Observations are @@ -312,3 +329,61 @@ func (h *Histogram) With(labelValues ...string) metrics.Histogram { func (h *Histogram) Observe(value float64) { h.obs(h.name, h.lvs, value) } + +type pair struct{ label, value string } + +type gaugeNode struct { + mtx sync.RWMutex + gauge *Gauge + children map[pair]*gaugeNode +} + +func (n *gaugeNode) addGauge(g *Gauge, lvs lv.LabelValues) *Gauge { + n.mtx.Lock() + defer n.mtx.Unlock() + if len(lvs) == 0 { + if n.gauge == nil { + n.gauge = g + } + return n.gauge + } + if len(lvs) < 2 { + panic("too few LabelValues; programmer error!") + } + head, tail := pair{lvs[0], lvs[1]}, lvs[2:] + if n.children == nil { + n.children = map[pair]*gaugeNode{} + } + child, ok := n.children[head] + if !ok { + child = &gaugeNode{} + n.children[head] = child + } + return child.addGauge(g, tail) +} + +func (n *gaugeNode) walk(fn func(string, lv.LabelValues, float64) bool) bool { + n.mtx.RLock() + defer n.mtx.RUnlock() + if n.gauge != nil { + value, ok := n.gauge.read() + if ok && !fn(n.gauge.g.Name, n.gauge.g.LabelValues(), value) { + return false + } + } + for _, child := range n.children { + if !child.walk(fn) { + return false + } + } + return true +} + +func (g *Gauge) touch() { + atomic.StoreInt32(&(g.set), 1) +} + +func (g *Gauge) read() (float64, bool) { + set := atomic.SwapInt32(&(g.set), 0) + return g.g.Value(), set != 0 +} diff --git a/metrics/dogstatsd/dogstatsd_test.go b/metrics/dogstatsd/dogstatsd_test.go index 2485cad97..ef6a5a458 100644 --- a/metrics/dogstatsd/dogstatsd_test.go +++ b/metrics/dogstatsd/dogstatsd_test.go @@ -29,8 +29,8 @@ func TestCounterSampled(t *testing.T) { func TestGauge(t *testing.T) { prefix, name := "ghi.", "jkl" label, value := "xyz", "abc" - regex := `^` + prefix + name + `:([0-9\.]+)\|g\|#` + label + `:` + value + `$` - d := New(prefix, log.NewNopLogger()) + regex := `^` + prefix + name + `:([0-9\.]+)\|g\|#hostname:foohost,` + label + `:` + value + `$` + d := New(prefix, log.NewNopLogger(), "hostname", "foohost") gauge := d.NewGauge(name).With(label, value) valuef := teststat.LastLine(d, regex) if err := teststat.TestGauge(gauge, valuef); err != nil { diff --git a/metrics/timer.go b/metrics/timer.go index c354df0f6..e12d9cd5c 100644 --- a/metrics/timer.go +++ b/metrics/timer.go @@ -7,6 +7,7 @@ import "time" type Timer struct { h Histogram t time.Time + u time.Duration } // NewTimer wraps the given histogram and records the current time. @@ -14,15 +15,22 @@ func NewTimer(h Histogram) *Timer { return &Timer{ h: h, t: time.Now(), + u: time.Second, } } // ObserveDuration captures the number of seconds since the timer was // constructed, and forwards that observation to the histogram. func (t *Timer) ObserveDuration() { - d := time.Since(t.t).Seconds() + d := float64(time.Since(t.t).Nanoseconds()) / float64(t.u) if d < 0 { d = 0 } t.h.Observe(d) } + +// Unit sets the unit of the float64 emitted by the timer. +// By default, the timer emits seconds. +func (t *Timer) Unit(u time.Duration) { + t.u = u +} diff --git a/metrics/timer_test.go b/metrics/timer_test.go index dedab2a70..2743e99b6 100644 --- a/metrics/timer_test.go +++ b/metrics/timer_test.go @@ -31,3 +31,28 @@ func TestTimerSlow(t *testing.T) { t.Errorf("want %.3f, have %.3f", want, have) } } + +func TestTimerUnit(t *testing.T) { + for _, tc := range []struct { + name string + unit time.Duration + tolerance float64 + want float64 + }{ + {"Seconds", time.Second, 0.010, 0.100}, + {"Milliseconds", time.Millisecond, 10, 100}, + {"Nanoseconds", time.Nanosecond, 10000000, 100000000}, + } { + t.Run(tc.name, func(t *testing.T) { + h := generic.NewSimpleHistogram() + timer := metrics.NewTimer(h) + time.Sleep(100 * time.Millisecond) + timer.Unit(tc.unit) + timer.ObserveDuration() + + if want, have := tc.want, h.ApproximateMovingAverage(); math.Abs(want-have) > tc.tolerance { + t.Errorf("want %.3f, have %.3f", want, have) + } + }) + } +} diff --git a/ratelimit/token_bucket.go b/ratelimit/token_bucket.go index b71e50bb1..e8a6de6f6 100644 --- a/ratelimit/token_bucket.go +++ b/ratelimit/token_bucket.go @@ -3,9 +3,6 @@ package ratelimit import ( "context" "errors" - "time" - - "github.com/juju/ratelimit" "github.com/go-kit/kit/endpoint" ) @@ -14,22 +11,6 @@ import ( // triggered and the request is rejected. var ErrLimited = errors.New("rate limit exceeded") -// NewTokenBucketLimiter returns an endpoint.Middleware that acts as a rate -// limiter based on a token-bucket algorithm. Requests that would exceed the -// maximum request rate are simply rejected with an error. -func NewTokenBucketLimiter(tb *ratelimit.Bucket) endpoint.Middleware { - return NewErroringLimiter(NewAllower(tb)) -} - -// NewTokenBucketThrottler returns an endpoint.Middleware that acts as a -// request throttler based on a token-bucket algorithm. Requests that would -// exceed the maximum request rate are delayed. -// The parameterized function "_" is kept for backwards-compatiblity of -// the API, but it is no longer used for anything. You may pass it nil. -func NewTokenBucketThrottler(tb *ratelimit.Bucket, _ func(time.Duration)) endpoint.Middleware { - return NewDelayingLimiter(NewWaiter(tb)) -} - // Allower dictates whether or not a request is acceptable to run. // The Limiter from "golang.org/x/time/rate" already implements this interface, // one is able to use that in NewErroringLimiter without any modifications. @@ -81,13 +62,6 @@ func (f AllowerFunc) Allow() bool { return f() } -// NewAllower turns an existing ratelimit.Bucket into an API-compatible form -func NewAllower(tb *ratelimit.Bucket) Allower { - return AllowerFunc(func() bool { - return (tb.TakeAvailable(1) != 0) - }) -} - // WaiterFunc is an adapter that lets a function operate as if // it implements Waiter type WaiterFunc func(ctx context.Context) error @@ -96,17 +70,3 @@ type WaiterFunc func(ctx context.Context) error func (f WaiterFunc) Wait(ctx context.Context) error { return f(ctx) } - -// NewWaiter turns an existing ratelimit.Bucket into an API-compatible form -func NewWaiter(tb *ratelimit.Bucket) Waiter { - return WaiterFunc(func(ctx context.Context) error { - dur := tb.Take(1) - select { - case <-ctx.Done(): - return ctx.Err() - case <-time.After(dur): - // happy path - } - return nil - }) -} diff --git a/ratelimit/token_bucket_test.go b/ratelimit/token_bucket_test.go index d444fe992..3845c9ee3 100644 --- a/ratelimit/token_bucket_test.go +++ b/ratelimit/token_bucket_test.go @@ -6,7 +6,6 @@ import ( "testing" "time" - jujuratelimit "github.com/juju/ratelimit" "golang.org/x/time/rate" "github.com/go-kit/kit/endpoint" @@ -15,22 +14,6 @@ import ( var nopEndpoint = func(context.Context, interface{}) (interface{}, error) { return struct{}{}, nil } -func TestTokenBucketLimiter(t *testing.T) { - tb := jujuratelimit.NewBucket(time.Minute, 1) - testSuccessThenFailure( - t, - ratelimit.NewTokenBucketLimiter(tb)(nopEndpoint), - ratelimit.ErrLimited.Error()) -} - -func TestTokenBucketThrottler(t *testing.T) { - tb := jujuratelimit.NewBucket(time.Minute, 1) - testSuccessThenFailure( - t, - ratelimit.NewTokenBucketThrottler(tb, nil)(nopEndpoint), - "context deadline exceeded") -} - func TestXRateErroring(t *testing.T) { limit := rate.NewLimiter(rate.Every(time.Minute), 1) testSuccessThenFailure( diff --git a/sd/consul/instancer_test.go b/sd/consul/instancer_test.go index 5df6b4351..ec7dd31b1 100644 --- a/sd/consul/instancer_test.go +++ b/sd/consul/instancer_test.go @@ -10,7 +10,7 @@ import ( "github.com/go-kit/kit/sd" ) -var _ sd.Instancer = &Instancer{} // API check +var _ sd.Instancer = (*Instancer)(nil) // API check var consulState = []*consul.ServiceEntry{ { diff --git a/sd/dnssrv/instancer_test.go b/sd/dnssrv/instancer_test.go index c3221bbca..6c8eca506 100644 --- a/sd/dnssrv/instancer_test.go +++ b/sd/dnssrv/instancer_test.go @@ -10,7 +10,7 @@ import ( "github.com/go-kit/kit/sd" ) -var _ sd.Instancer = &Instancer{} // API check +var _ sd.Instancer = (*Instancer)(nil) // API check func TestRefresh(t *testing.T) { name := "some.service.internal" diff --git a/sd/endpointer.go b/sd/endpointer.go index bd277a7bf..5c98fc753 100644 --- a/sd/endpointer.go +++ b/sd/endpointer.go @@ -78,7 +78,7 @@ func (de *DefaultEndpointer) receive() { } } -// Close de-registeres DefaultEndpointer from the Instancer and stops the internal go-routine. +// Close deregisters DefaultEndpointer from the Instancer and stops the internal go-routine. func (de *DefaultEndpointer) Close() { de.instancer.Deregister(de.ch) close(de.ch) diff --git a/sd/endpointer_test.go b/sd/endpointer_test.go index bea6605d4..671178f69 100644 --- a/sd/endpointer_test.go +++ b/sd/endpointer_test.go @@ -19,33 +19,40 @@ func TestDefaultEndpointer(t *testing.T) { f = func(instance string) (endpoint.Endpoint, io.Closer, error) { return endpoint.Nop, c[instance], nil } - instancer = &mockInstancer{ - cache: instance.NewCache(), - } + instancer = &mockInstancer{instance.NewCache()} ) // set initial state instancer.Update(sd.Event{Instances: []string{"a", "b"}}) endpointer := sd.NewEndpointer(instancer, f, log.NewNopLogger(), sd.InvalidateOnError(time.Minute)) - if endpoints, err := endpointer.Endpoints(); err != nil { - t.Errorf("unepected error %v", err) - } else if want, have := 2, len(endpoints); want != have { - t.Errorf("want %d, have %d", want, have) + + var ( + endpoints []endpoint.Endpoint + err error + ) + if !within(time.Second, func() bool { + endpoints, err = endpointer.Endpoints() + return err == nil && len(endpoints) == 2 + }) { + t.Errorf("wanted 2 endpoints, got %d (%v)", len(endpoints), err) } instancer.Update(sd.Event{Instances: []string{}}) + select { case <-ca: t.Logf("endpoint a closed, good") case <-time.After(time.Millisecond): t.Errorf("didn't close the deleted instance in time") } + select { case <-cb: t.Logf("endpoint b closed, good") case <-time.After(time.Millisecond): t.Errorf("didn't close the deleted instance in time") } + if endpoints, err := endpointer.Endpoints(); err != nil { t.Errorf("unepected error %v", err) } else if want, have := 0, len(endpoints); want != have { @@ -53,28 +60,26 @@ func TestDefaultEndpointer(t *testing.T) { } endpointer.Close() + instancer.Update(sd.Event{Instances: []string{"a"}}) // TODO verify that on Close the endpointer fully disconnects from the instancer. // Unfortunately, because we use instance.Cache, this test cannot be in the sd package, // and therefore does not have access to the endpointer's private members. } -type mockInstancer struct { - cache *instance.Cache -} - -func (m *mockInstancer) Update(event sd.Event) { - m.cache.Update(event) -} - -func (m *mockInstancer) Register(ch chan<- sd.Event) { - m.cache.Register(ch) -} - -func (m *mockInstancer) Deregister(ch chan<- sd.Event) { - m.cache.Deregister(ch) -} +type mockInstancer struct{ *instance.Cache } type closer chan struct{} func (c closer) Close() error { close(c); return nil } + +func within(d time.Duration, f func() bool) bool { + deadline := time.Now().Add(d) + for time.Now().Before(deadline) { + if f() { + return true + } + time.Sleep(d / 10) + } + return false +} diff --git a/sd/etcd/instancer_test.go b/sd/etcd/instancer_test.go index 9609e2839..5e18e1098 100644 --- a/sd/etcd/instancer_test.go +++ b/sd/etcd/instancer_test.go @@ -10,6 +10,8 @@ import ( "github.com/go-kit/kit/sd" ) +var _ sd.Instancer = (*Instancer)(nil) // API check + var ( node = &stdetcd.Node{ Key: "/foo", diff --git a/sd/eureka/instancer_test.go b/sd/eureka/instancer_test.go index cde4c61ae..9363e5055 100644 --- a/sd/eureka/instancer_test.go +++ b/sd/eureka/instancer_test.go @@ -9,7 +9,7 @@ import ( "github.com/go-kit/kit/sd" ) -var _ sd.Instancer = &Instancer{} // API check +var _ sd.Instancer = (*Instancer)(nil) // API check func TestInstancer(t *testing.T) { connection := &testConnection{ diff --git a/sd/instancer.go b/sd/instancer.go index 09647a944..0a8e6c696 100644 --- a/sd/instancer.go +++ b/sd/instancer.go @@ -22,6 +22,7 @@ type Event struct { type Instancer interface { Register(chan<- Event) Deregister(chan<- Event) + Stop() } // FixedInstancer yields a fixed set of instances. @@ -32,3 +33,6 @@ func (d FixedInstancer) Register(ch chan<- Event) { ch <- Event{Instances: d} } // Deregister implements Instancer. func (d FixedInstancer) Deregister(ch chan<- Event) {} + +// Stop implements Instancer. +func (d FixedInstancer) Stop() {} diff --git a/sd/internal/instance/cache.go b/sd/internal/instance/cache.go index 94b19fdf0..27b2122a1 100644 --- a/sd/internal/instance/cache.go +++ b/sd/internal/instance/cache.go @@ -45,6 +45,10 @@ func (c *Cache) State() sd.Event { return c.state } +// Stop implements Instancer. Since the cache is just a plain-old store of data, +// Stop is a no-op. +func (c *Cache) Stop() {} + // Register implements Instancer. func (c *Cache) Register(ch chan<- sd.Event) { c.mtx.Lock() diff --git a/sd/internal/instance/cache_test.go b/sd/internal/instance/cache_test.go index 0d77e0dbc..05a1cc270 100644 --- a/sd/internal/instance/cache_test.go +++ b/sd/internal/instance/cache_test.go @@ -8,7 +8,7 @@ import ( "github.com/go-kit/kit/sd" ) -var _ sd.Instancer = &Cache{} // API check +var _ sd.Instancer = (*Cache)(nil) // API check // The test verifies the following: // registering causes initial notification of the current state diff --git a/sd/zk/instancer_test.go b/sd/zk/instancer_test.go index de9666b66..c450c3e60 100644 --- a/sd/zk/instancer_test.go +++ b/sd/zk/instancer_test.go @@ -7,7 +7,7 @@ import ( "github.com/go-kit/kit/sd" ) -var _ sd.Instancer = &Instancer{} +var _ sd.Instancer = (*Instancer)(nil) // API check func TestInstancer(t *testing.T) { client := newFakeClient() diff --git a/transport/grpc/client.go b/transport/grpc/client.go index 535b70f24..28c203f82 100644 --- a/transport/grpc/client.go +++ b/transport/grpc/client.go @@ -91,7 +91,7 @@ func (c Client) Endpoint() endpoint.Endpoint { for _, f := range c.before { ctx = f(ctx, md) } - ctx = metadata.NewContext(ctx, *md) + ctx = metadata.NewOutgoingContext(ctx, *md) var header, trailer metadata.MD grpcReply := reflect.New(c.grpcReply).Interface() diff --git a/transport/grpc/server.go b/transport/grpc/server.go index b14d7d8db..6da4bdb97 100644 --- a/transport/grpc/server.go +++ b/transport/grpc/server.go @@ -73,7 +73,7 @@ func ServerErrorLogger(logger log.Logger) ServerOption { // ServeGRPC implements the Handler interface. func (s Server) ServeGRPC(ctx oldcontext.Context, req interface{}) (oldcontext.Context, interface{}, error) { // Retrieve gRPC metadata. - md, ok := metadata.FromContext(ctx) + md, ok := metadata.FromIncomingContext(ctx) if !ok { md = metadata.MD{} } diff --git a/update_deps.bash b/update_deps.bash index 0a349a88a..576e8b4a5 100755 --- a/update_deps.bash +++ b/update_deps.bash @@ -20,7 +20,7 @@ function go_get_update { while read d do echo $d - go get -u $d + go get -u $d || echo "failed, trying again with master" && cd $GOPATH/src/$d && git checkout master && go get -u $d done } diff --git a/util/conn/manager_test.go b/util/conn/manager_test.go index 5e41b31b9..c91b70629 100644 --- a/util/conn/manager_test.go +++ b/util/conn/manager_test.go @@ -40,7 +40,7 @@ func TestManager(t *testing.T) { // First takes should fail. for i := 0; i < 10; i++ { if conn = mgr.Take(); conn != nil { - t.Fatalf("want nil conn, got real conn") + t.Fatalf("iteration %d: want nil conn, got real conn", i) } }