-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathstatic_client.go
280 lines (241 loc) · 7.64 KB
/
static_client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
package api2
import (
"bytes"
"fmt"
"go/parser"
"go/token"
"log"
"os"
"path/filepath"
"reflect"
"regexp"
"runtime"
"strings"
"text/template"
)
func getMethod(handler interface{}) (name, request, response string, err error) {
// Get request and response names.
handlerFunc := handler
type Funcer interface {
Func() interface{}
}
funcer, ok := handler.(Funcer)
if ok {
handlerFunc = funcer.Func()
}
handlerType := reflect.TypeOf(handlerFunc)
request = handlerType.In(1).Elem().Name()
response = handlerType.Out(0).Elem().Name()
// Try to extract method name from a result of api2.Method().
type FuncInfoer interface {
FuncInfo() (pkgFull, pkgName, structName, method string)
}
fi, ok := handler.(FuncInfoer)
if ok {
_, _, _, name = fi.FuncInfo()
return
}
// Fallback: try to get method name from request and response names.
conventions := []struct {
request, response string
}{
{request: "Request", response: "Response"},
{request: "Args", response: "Reply"},
{request: "Req", response: "Resp"},
{request: "Req", response: "Res"},
}
for _, conv := range conventions {
nameFromReq := strings.TrimSuffix(request, conv.request)
nameFromRes := strings.TrimSuffix(response, conv.response)
if nameFromReq == nameFromRes {
name = nameFromReq
return
}
}
err = fmt.Errorf("can not deduce method name. Use api2.Method or name your request and response types like FooRequest and FooResponse")
return
}
func getFileByFunction(f interface{}) string {
rf := runtime.FuncForPC(reflect.ValueOf(f).Pointer())
fileName, _ := rf.FileLine(rf.Entry())
return fileName
}
func getFuncName(f interface{}) string {
fullName := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()
// Cut package path from the function name.
index := strings.LastIndex(fullName, ".")
return fullName[index+1:]
}
func runTemplate(routes []Route, pkg, api2pkg string, getRoutesNames, serviceInterfaces []string) (code string, err error) {
type Method struct {
Name string
Request string
Response string
}
type Vars struct {
Pkg string
Api2Pkg string
GetRoutesNames []string
ServiceInterfaces []string
Methods []Method
}
methods := make([]Method, 0, len(routes))
for i, r := range routes {
name, request, response, err := getMethod(r.Handler)
if err != nil {
return "", fmt.Errorf("failed to analyze method %d in package %s: %v", i, pkg, err)
}
methods = append(methods, Method{
Name: name,
Request: request,
Response: response,
})
}
vars := Vars{
Pkg: pkg,
Api2Pkg: api2pkg,
GetRoutesNames: getRoutesNames,
ServiceInterfaces: serviceInterfaces,
Methods: methods,
}
var codeBuf bytes.Buffer
if err := clientTemplate.Execute(&codeBuf, vars); err != nil {
return "", fmt.Errorf("template executation failed for package %s: %v", pkg, err)
}
return codeBuf.String(), nil
}
var clientTemplate = template.Must(template.New("static_client").Parse(clientTemplateStr))
const clientTemplateStr = `package {{ .Pkg }}
// Code generated by api2. DO NOT EDIT.
import (
"context"
"net/url"
"{{ .Api2Pkg }}"
)
type Client struct {
api2client *api2.Client
}
{{ range .ServiceInterfaces }}
var _ {{ . }} = (*Client)(nil)
{{ end }}
func NewClient(baseURL string, opts ...api2.Option) (*Client, error) {
if _, err := url.ParseRequestURI(baseURL); err != nil {
return nil, err
}
var routes []api2.Route
{{ range .GetRoutesNames }}
routes = append(routes, {{ . }}(nil)...)
{{ end }}
api2client := api2.NewClient(routes, baseURL, opts...)
return &Client{
api2client: api2client,
}, nil
}
func (c *Client) Close() error {
return c.api2client.Close()
}
{{ range .Methods }}
func (c *Client) {{ .Name }}(ctx context.Context, req *{{ .Request }}) (res *{{ .Response }}, err error) {
res = &{{ .Response }}{}
err = c.api2client.Call(ctx, res, req)
if err != nil {
return nil, err
}
return
}
{{end}}`
// GenerateClientCode accepts global function GetRoutes of a package and
// returns the code of static client and path to the file where the code
// should be saved (client.go in the same directory where GetRoutes and
// types of requests, responses and service are defined.
func GenerateClientCode(getRoutess ...interface{}) (code, clientFile string, err error) {
var routes []Route
var getRoutesNames, serviceInterfaces []string
var pkg, api2pkg string
for _, getRoutes := range getRoutess {
fileName := getFileByFunction(getRoutes)
// Check that the file does not exist or was generated.
dir := filepath.Dir(fileName)
clientFile = filepath.Join(dir, "client.go")
pkg1, api2pkg1, err := detectPkgs(dir)
if err != nil {
return "", "", fmt.Errorf("failed to determine pkg and api2pkg for dir %s: %v", dir, err)
}
if pkg != "" && pkg1 != pkg {
return "", "", fmt.Errorf("instances of GetRoutes belong to different directories: %s and %s", pkg, pkg1)
}
if api2pkg != "" && api2pkg1 != api2pkg {
return "", "", fmt.Errorf("instances of GetRoutes use different api2 packages: %s and %s", api2pkg, api2pkg1)
}
pkg = pkg1
api2pkg = api2pkg1
genValue := reflect.ValueOf(getRoutes)
serviceArg := reflect.New(genValue.Type().In(0)).Elem()
routesValues := genValue.Call([]reflect.Value{serviceArg})
routes = append(routes, routesValues[0].Interface().([]Route)...)
getRoutesNames = append(getRoutesNames, getFuncName(getRoutes))
if genValue.Type().In(0).Kind() == reflect.Interface {
serviceInterfaces = append(serviceInterfaces, genValue.Type().In(0).Name())
}
}
code, err = runTemplate(routes, pkg, api2pkg, getRoutesNames, serviceInterfaces)
if err != nil {
return "", "", err
}
return code, clientFile, nil
}
// GenerateClient generates file client.go with static client near the file
// in which passed GetRoutes function is defined.
func GenerateClient(getRoutes ...interface{}) {
fileName := getFileByFunction(getRoutes[0])
log.Printf("api2 generates static client for %s ...", fileName)
if err := generateClient(getRoutes...); err != nil {
log.Fatalf("api2 failed to generate static client for %s: %v", fileName, err)
}
}
var codeGeneratedRE = regexp.MustCompile(`.*Code generated .* DO NOT EDIT\..*`)
func generateClient(getRoutes ...interface{}) error {
code, clientFile, err := GenerateClientCode(getRoutes...)
if err != nil {
return err
}
// Check that the file does not exist or was generated.
oldContent, err := os.ReadFile(clientFile)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("unknown error when reading from %s: %v", clientFile, err)
} else if err == nil && !codeGeneratedRE.Match(oldContent) {
return fmt.Errorf("file %s exists and was not generated; please remove it to proceed", clientFile)
}
if err := os.WriteFile(clientFile, []byte(code), 0644); err != nil {
return fmt.Errorf("failed to write file %s: %v", clientFile, err)
}
return nil
}
func detectPkgs(dir string) (pkg, api2pkg string, err error) {
fset := token.NewFileSet()
pkgs, err := parser.ParseDir(fset, dir, nil, parser.ImportsOnly)
if err != nil {
return "", "", err
}
var pkgNames []string
for pkgName := range pkgs {
pkgNames = append(pkgNames, pkgName)
}
if len(pkgNames) != 1 {
return "", "", fmt.Errorf("found %d packages (%v) in dir %s, want %d", len(pkgNames), pkgNames, dir, 1)
}
pkg = pkgNames[0]
for _, gofile := range pkgs[pkg].Files {
for _, importSpec := range gofile.Imports {
importPath := importSpec.Path.Value
importPath = strings.TrimPrefix(importPath, `"`)
importPath = strings.TrimSuffix(importPath, `"`)
if !strings.HasSuffix(importPath, "/api2") {
continue
}
api2pkg = importPath
return
}
}
return "", "", fmt.Errorf("failed to find import of api2 in dir %s", dir)
}