github.com/mattermosttest/mattermost-server/v5@v5.0.0-20200917143240-9dfa12e121f9/plugin/interface_generator/main.go (about) 1 // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. 2 // See LICENSE.txt for license information. 3 4 package main 5 6 import ( 7 "bytes" 8 "fmt" 9 "go/ast" 10 "go/parser" 11 "go/printer" 12 "go/token" 13 "io/ioutil" 14 "log" 15 "os/exec" 16 "path/filepath" 17 "strings" 18 "text/template" 19 20 "github.com/pkg/errors" 21 "golang.org/x/tools/imports" 22 ) 23 24 type IHookEntry struct { 25 FuncName string 26 Args *ast.FieldList 27 Results *ast.FieldList 28 } 29 30 type PluginInterfaceInfo struct { 31 Hooks []IHookEntry 32 API []IHookEntry 33 FileSet *token.FileSet 34 } 35 36 func FieldListToFuncList(fieldList *ast.FieldList, fileset *token.FileSet) string { 37 result := []string{} 38 if fieldList == nil || len(fieldList.List) == 0 { 39 return "()" 40 } 41 for _, field := range fieldList.List { 42 typeNameBuffer := &bytes.Buffer{} 43 err := printer.Fprint(typeNameBuffer, fileset, field.Type) 44 if err != nil { 45 panic(err) 46 } 47 typeName := typeNameBuffer.String() 48 names := []string{} 49 for _, name := range field.Names { 50 names = append(names, name.Name) 51 } 52 result = append(result, strings.Join(names, ", ")+" "+typeName) 53 } 54 55 return "(" + strings.Join(result, ", ") + ")" 56 } 57 58 func FieldListToNames(fieldList *ast.FieldList, fileset *token.FileSet, variadicForm bool) string { 59 result := []string{} 60 if fieldList == nil || len(fieldList.List) == 0 { 61 return "" 62 } 63 for _, field := range fieldList.List { 64 for _, name := range field.Names { 65 paramName := name.Name 66 if _, ok := field.Type.(*ast.Ellipsis); ok && variadicForm { 67 paramName = fmt.Sprintf("%s...", paramName) 68 } 69 result = append(result, paramName) 70 } 71 } 72 73 return strings.Join(result, ", ") 74 } 75 76 func FieldListToEncodedErrors(structPrefix string, fieldList *ast.FieldList, fileset *token.FileSet) string { 77 result := []string{} 78 if fieldList == nil { 79 return "" 80 } 81 82 nextLetter := 'A' 83 for _, field := range fieldList.List { 84 typeNameBuffer := &bytes.Buffer{} 85 err := printer.Fprint(typeNameBuffer, fileset, field.Type) 86 if err != nil { 87 panic(err) 88 } 89 90 if typeNameBuffer.String() != "error" { 91 nextLetter++ 92 continue 93 } 94 95 name := "" 96 if len(field.Names) == 0 { 97 name = string(nextLetter) 98 nextLetter++ 99 } else { 100 for range field.Names { 101 name += string(nextLetter) 102 nextLetter++ 103 } 104 } 105 106 result = append(result, structPrefix+name+" = encodableError("+structPrefix+name+")") 107 108 } 109 110 return strings.Join(result, "\n") 111 } 112 113 func FieldListDestruct(structPrefix string, fieldList *ast.FieldList, fileset *token.FileSet) string { 114 result := []string{} 115 if fieldList == nil || len(fieldList.List) == 0 { 116 return "" 117 } 118 nextLetter := 'A' 119 for _, field := range fieldList.List { 120 typeNameBuffer := &bytes.Buffer{} 121 err := printer.Fprint(typeNameBuffer, fileset, field.Type) 122 if err != nil { 123 panic(err) 124 } 125 typeName := typeNameBuffer.String() 126 suffix := "" 127 if strings.HasPrefix(typeName, "...") { 128 suffix = "..." 129 } 130 if len(field.Names) == 0 { 131 result = append(result, structPrefix+string(nextLetter)+suffix) 132 nextLetter++ 133 } else { 134 for range field.Names { 135 result = append(result, structPrefix+string(nextLetter)+suffix) 136 nextLetter++ 137 } 138 } 139 } 140 141 return strings.Join(result, ", ") 142 } 143 144 func FieldListToRecordSuccess(structPrefix string, fieldList *ast.FieldList, fileset *token.FileSet) string { 145 if fieldList == nil || len(fieldList.List) == 0 { 146 return "true" 147 } 148 149 result := "" 150 nextLetter := 'A' 151 for _, field := range fieldList.List { 152 typeName := baseTypeName(field.Type) 153 if typeName == "error" || typeName == "AppError" { 154 result = structPrefix + string(nextLetter) 155 break 156 } 157 nextLetter++ 158 } 159 160 if result == "" { 161 return "true" 162 } 163 return fmt.Sprintf("%s == nil", result) 164 } 165 166 func FieldListToStructList(fieldList *ast.FieldList, fileset *token.FileSet) string { 167 result := []string{} 168 if fieldList == nil || len(fieldList.List) == 0 { 169 return "" 170 } 171 nextLetter := 'A' 172 for _, field := range fieldList.List { 173 typeNameBuffer := &bytes.Buffer{} 174 err := printer.Fprint(typeNameBuffer, fileset, field.Type) 175 if err != nil { 176 panic(err) 177 } 178 typeName := typeNameBuffer.String() 179 if strings.HasPrefix(typeName, "...") { 180 typeName = strings.Replace(typeName, "...", "[]", 1) 181 } 182 if len(field.Names) == 0 { 183 result = append(result, string(nextLetter)+" "+typeName) 184 nextLetter++ 185 } else { 186 for range field.Names { 187 result = append(result, string(nextLetter)+" "+typeName) 188 nextLetter++ 189 } 190 } 191 } 192 193 return strings.Join(result, "\n\t") 194 } 195 196 func baseTypeName(x ast.Expr) string { 197 switch t := x.(type) { 198 case *ast.Ident: 199 return t.Name 200 case *ast.SelectorExpr: 201 if _, ok := t.X.(*ast.Ident); ok { 202 // only possible for qualified type names; 203 // assume type is imported 204 return t.Sel.Name 205 } 206 case *ast.ParenExpr: 207 return baseTypeName(t.X) 208 case *ast.StarExpr: 209 return baseTypeName(t.X) 210 } 211 return "" 212 } 213 214 func goList(dir string) ([]string, error) { 215 cmd := exec.Command("go", "list", "-f", "{{.Dir}}", dir) 216 bytes, err := cmd.Output() 217 if err != nil { 218 return nil, errors.Wrap(err, "Can't list packages") 219 } 220 221 return strings.Fields(string(bytes)), nil 222 } 223 224 func (info *PluginInterfaceInfo) addHookMethod(method *ast.Field) { 225 info.Hooks = append(info.Hooks, IHookEntry{ 226 FuncName: method.Names[0].Name, 227 Args: method.Type.(*ast.FuncType).Params, 228 Results: method.Type.(*ast.FuncType).Results, 229 }) 230 } 231 232 func (info *PluginInterfaceInfo) addAPIMethod(method *ast.Field) { 233 info.API = append(info.API, IHookEntry{ 234 FuncName: method.Names[0].Name, 235 Args: method.Type.(*ast.FuncType).Params, 236 Results: method.Type.(*ast.FuncType).Results, 237 }) 238 } 239 240 func (info *PluginInterfaceInfo) makeHookInspector() func(node ast.Node) bool { 241 return func(node ast.Node) bool { 242 if typeSpec, ok := node.(*ast.TypeSpec); ok { 243 if typeSpec.Name.Name == "Hooks" { 244 for _, method := range typeSpec.Type.(*ast.InterfaceType).Methods.List { 245 info.addHookMethod(method) 246 } 247 return false 248 } else if typeSpec.Name.Name == "API" { 249 for _, method := range typeSpec.Type.(*ast.InterfaceType).Methods.List { 250 info.addAPIMethod(method) 251 } 252 return false 253 } 254 } 255 return true 256 } 257 } 258 259 func getPluginInfo(dir string) (*PluginInterfaceInfo, error) { 260 pluginInfo := &PluginInterfaceInfo{ 261 Hooks: make([]IHookEntry, 0), 262 FileSet: token.NewFileSet(), 263 } 264 265 packages, err := parser.ParseDir(pluginInfo.FileSet, dir, nil, parser.ParseComments) 266 if err != nil { 267 log.Println("Parser error in dir "+dir+": ", err) 268 } 269 270 for _, pkg := range packages { 271 if pkg.Name != "plugin" { 272 continue 273 } 274 275 for _, file := range pkg.Files { 276 ast.Inspect(file, pluginInfo.makeHookInspector()) 277 } 278 } 279 280 return pluginInfo, nil 281 } 282 283 var hooksTemplate = `// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. 284 // See LICENSE.txt for license information. 285 286 // Code generated by "make pluginapi" 287 // DO NOT EDIT 288 289 package plugin 290 291 {{range .HooksMethods}} 292 293 func init() { 294 hookNameToId["{{.Name}}"] = {{.Name}}Id 295 } 296 297 type {{.Name | obscure}}Args struct { 298 {{structStyle .Params}} 299 } 300 301 type {{.Name | obscure}}Returns struct { 302 {{structStyle .Return}} 303 } 304 305 func (g *hooksRPCClient) {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} { 306 _args := &{{.Name | obscure}}Args{ {{valuesOnly .Params}} } 307 _returns := &{{.Name | obscure}}Returns{} 308 if g.implemented[{{.Name}}Id] { 309 if err := g.client.Call("Plugin.{{.Name}}", _args, _returns); err != nil { 310 g.log.Error("RPC call {{.Name}} to plugin failed.", mlog.Err(err)) 311 } 312 } 313 {{ if .Return }} return {{destruct "_returns." .Return}} {{ end }} 314 } 315 316 func (s *hooksRPCServer) {{.Name}}(args *{{.Name | obscure}}Args, returns *{{.Name | obscure}}Returns) error { 317 if hook, ok := s.impl.(interface { 318 {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} 319 }); ok { 320 {{if .Return}}{{destruct "returns." .Return}} = {{end}}hook.{{.Name}}({{destruct "args." .Params}}) 321 {{if .Return}}{{encodeErrors "returns." .Return}}{{end -}} 322 } else { 323 return encodableError(fmt.Errorf("Hook {{.Name}} called but not implemented.")) 324 } 325 return nil 326 } 327 {{end}} 328 329 {{range .APIMethods}} 330 331 type {{.Name | obscure}}Args struct { 332 {{structStyle .Params}} 333 } 334 335 type {{.Name | obscure}}Returns struct { 336 {{structStyle .Return}} 337 } 338 339 func (g *apiRPCClient) {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} { 340 _args := &{{.Name | obscure}}Args{ {{valuesOnly .Params}} } 341 _returns := &{{.Name | obscure}}Returns{} 342 if err := g.client.Call("Plugin.{{.Name}}", _args, _returns); err != nil { 343 log.Printf("RPC call to {{.Name}} API failed: %s", err.Error()) 344 } 345 {{ if .Return }} return {{destruct "_returns." .Return}} {{ end }} 346 } 347 348 func (s *apiRPCServer) {{.Name}}(args *{{.Name | obscure}}Args, returns *{{.Name | obscure}}Returns) error { 349 if hook, ok := s.impl.(interface { 350 {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} 351 }); ok { 352 {{if .Return}}{{destruct "returns." .Return}} = {{end}}hook.{{.Name}}({{destruct "args." .Params}}) 353 {{if .Return}}{{encodeErrors "returns." .Return}}{{end -}} 354 } else { 355 return encodableError(fmt.Errorf("API {{.Name}} called but not implemented.")) 356 } 357 return nil 358 } 359 {{end}} 360 ` 361 362 var apiTimerLayerTemplate = `// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. 363 // See LICENSE.txt for license information. 364 365 // Code generated by "make pluginapi" 366 // DO NOT EDIT 367 368 package plugin 369 370 import ( 371 "io" 372 "net/http" 373 timePkg "time" 374 375 "github.com/mattermost/mattermost-server/v5/einterfaces" 376 "github.com/mattermost/mattermost-server/v5/model" 377 ) 378 379 type apiTimerLayer struct { 380 pluginID string 381 apiImpl API 382 metrics einterfaces.MetricsInterface 383 } 384 385 func (api *apiTimerLayer) recordTime(startTime timePkg.Time, name string, success bool) { 386 if api.metrics != nil { 387 elapsedTime := float64(timePkg.Since(startTime)) / float64(timePkg.Second) 388 api.metrics.ObservePluginApiDuration(api.pluginID, name, success, elapsedTime) 389 } 390 } 391 392 {{range .APIMethods}} 393 394 func (api *apiTimerLayer) {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} { 395 startTime := timePkg.Now() 396 {{ if .Return }} {{destruct "_returns" .Return}} := {{ end }} api.apiImpl.{{.Name}}({{valuesOnly .Params}}) 397 api.recordTime(startTime, "{{.Name}}", {{ shouldRecordSuccess "_returns" .Return }}) 398 {{ if .Return }} return {{destruct "_returns" .Return}} {{ end -}} 399 } 400 401 {{end}} 402 ` 403 404 var hooksTimerLayerTemplate = `// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. 405 // See LICENSE.txt for license information. 406 407 // Code generated by "make pluginapi" 408 // DO NOT EDIT 409 410 package plugin 411 412 import ( 413 "io" 414 "net/http" 415 timePkg "time" 416 417 "github.com/mattermost/mattermost-server/v5/einterfaces" 418 "github.com/mattermost/mattermost-server/v5/model" 419 ) 420 421 type hooksTimerLayer struct { 422 pluginID string 423 hooksImpl Hooks 424 metrics einterfaces.MetricsInterface 425 } 426 427 func (hooks *hooksTimerLayer) recordTime(startTime timePkg.Time, name string, success bool) { 428 if hooks.metrics != nil { 429 elapsedTime := float64(timePkg.Since(startTime)) / float64(timePkg.Second) 430 hooks.metrics.ObservePluginHookDuration(hooks.pluginID, name, success, elapsedTime) 431 } 432 } 433 434 {{range .HooksMethods}} 435 436 func (hooks *hooksTimerLayer) {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} { 437 startTime := timePkg.Now() 438 {{ if .Return }} {{destruct "_returns" .Return}} := {{ end }} hooks.hooksImpl.{{.Name}}({{valuesOnly .Params}}) 439 hooks.recordTime(startTime, "{{.Name}}", {{ shouldRecordSuccess "_returns" .Return }}) 440 {{ if .Return }} return {{destruct "_returns" .Return}} {{end -}} 441 } 442 443 {{end}} 444 ` 445 446 type MethodParams struct { 447 Name string 448 Params *ast.FieldList 449 Return *ast.FieldList 450 } 451 452 type HooksTemplateParams struct { 453 HooksMethods []MethodParams 454 APIMethods []MethodParams 455 } 456 457 func generateHooksGlue(info *PluginInterfaceInfo) { 458 templateFunctions := map[string]interface{}{ 459 "funcStyle": func(fields *ast.FieldList) string { return FieldListToFuncList(fields, info.FileSet) }, 460 "structStyle": func(fields *ast.FieldList) string { return FieldListToStructList(fields, info.FileSet) }, 461 "valuesOnly": func(fields *ast.FieldList) string { return FieldListToNames(fields, info.FileSet, false) }, 462 "encodeErrors": func(structPrefix string, fields *ast.FieldList) string { 463 return FieldListToEncodedErrors(structPrefix, fields, info.FileSet) 464 }, 465 "destruct": func(structPrefix string, fields *ast.FieldList) string { 466 return FieldListDestruct(structPrefix, fields, info.FileSet) 467 }, 468 "shouldRecordSuccess": func(structPrefix string, fields *ast.FieldList) string { 469 return FieldListToRecordSuccess(structPrefix, fields, info.FileSet) 470 }, 471 "obscure": func(name string) string { 472 return "Z_" + name 473 }, 474 } 475 476 hooksTemplate, err := template.New("hooks").Funcs(templateFunctions).Parse(hooksTemplate) 477 if err != nil { 478 panic(err) 479 } 480 481 templateParams := HooksTemplateParams{} 482 for _, hook := range info.Hooks { 483 templateParams.HooksMethods = append(templateParams.HooksMethods, MethodParams{ 484 Name: hook.FuncName, 485 Params: hook.Args, 486 Return: hook.Results, 487 }) 488 } 489 for _, api := range info.API { 490 templateParams.APIMethods = append(templateParams.APIMethods, MethodParams{ 491 Name: api.FuncName, 492 Params: api.Args, 493 Return: api.Results, 494 }) 495 } 496 templateResult := &bytes.Buffer{} 497 hooksTemplate.Execute(templateResult, &templateParams) 498 499 formatted, err := imports.Process("", templateResult.Bytes(), nil) 500 if err != nil { 501 panic(err) 502 } 503 504 if err := ioutil.WriteFile(filepath.Join(getPluginPackageDir(), "client_rpc_generated.go"), formatted, 0664); err != nil { 505 panic(err) 506 } 507 } 508 509 func generatePluginTimerLayer(info *PluginInterfaceInfo) { 510 templateFunctions := map[string]interface{}{ 511 "funcStyle": func(fields *ast.FieldList) string { return FieldListToFuncList(fields, info.FileSet) }, 512 "structStyle": func(fields *ast.FieldList) string { return FieldListToStructList(fields, info.FileSet) }, 513 "valuesOnly": func(fields *ast.FieldList) string { return FieldListToNames(fields, info.FileSet, true) }, 514 "destruct": func(structPrefix string, fields *ast.FieldList) string { 515 return FieldListDestruct(structPrefix, fields, info.FileSet) 516 }, 517 "shouldRecordSuccess": func(structPrefix string, fields *ast.FieldList) string { 518 return FieldListToRecordSuccess(structPrefix, fields, info.FileSet) 519 }, 520 } 521 522 // Prepare template params 523 templateParams := HooksTemplateParams{} 524 for _, hook := range info.Hooks { 525 templateParams.HooksMethods = append(templateParams.HooksMethods, MethodParams{ 526 Name: hook.FuncName, 527 Params: hook.Args, 528 Return: hook.Results, 529 }) 530 } 531 for _, api := range info.API { 532 templateParams.APIMethods = append(templateParams.APIMethods, MethodParams{ 533 Name: api.FuncName, 534 Params: api.Args, 535 Return: api.Results, 536 }) 537 } 538 539 pluginTemplates := map[string]string{ 540 "api_timer_layer_generated.go": apiTimerLayerTemplate, 541 "hooks_timer_layer_generated.go": hooksTimerLayerTemplate, 542 } 543 544 for fileName, presetTemplate := range pluginTemplates { 545 parsedTemplate, err := template.New("hooks").Funcs(templateFunctions).Parse(presetTemplate) 546 if err != nil { 547 panic(err) 548 } 549 550 templateResult := &bytes.Buffer{} 551 parsedTemplate.Execute(templateResult, &templateParams) 552 553 formatted, err := imports.Process("", templateResult.Bytes(), nil) 554 if err != nil { 555 panic(err) 556 } 557 558 if err := ioutil.WriteFile(filepath.Join(getPluginPackageDir(), fileName), formatted, 0664); err != nil { 559 panic(err) 560 } 561 } 562 } 563 564 func getPluginPackageDir() string { 565 dirs, err := goList("github.com/mattermost/mattermost-server/v5/plugin") 566 if err != nil { 567 panic(err) 568 } else if len(dirs) != 1 { 569 panic("More than one package dir, or no dirs!") 570 } 571 572 return dirs[0] 573 } 574 575 func removeExcluded(info *PluginInterfaceInfo) *PluginInterfaceInfo { 576 toBeExcluded := func(item string) bool { 577 excluded := []string{ 578 "FileWillBeUploaded", 579 "Implemented", 580 "LoadPluginConfiguration", 581 "InstallPlugin", 582 "LogDebug", 583 "LogError", 584 "LogInfo", 585 "LogWarn", 586 "MessageWillBePosted", 587 "MessageWillBeUpdated", 588 "OnActivate", 589 "PluginHTTP", 590 "ServeHTTP", 591 } 592 for _, exclusion := range excluded { 593 if exclusion == item { 594 return true 595 } 596 } 597 return false 598 } 599 hooksResult := make([]IHookEntry, 0, len(info.Hooks)) 600 for _, hook := range info.Hooks { 601 if !toBeExcluded(hook.FuncName) { 602 hooksResult = append(hooksResult, hook) 603 } 604 } 605 info.Hooks = hooksResult 606 607 apiResult := make([]IHookEntry, 0, len(info.API)) 608 for _, api := range info.API { 609 if !toBeExcluded(api.FuncName) { 610 apiResult = append(apiResult, api) 611 } 612 } 613 info.API = apiResult 614 615 return info 616 } 617 618 func main() { 619 pluginPackageDir := getPluginPackageDir() 620 621 log.Println("Generating plugin hooks glue") 622 forRPC, err := getPluginInfo(pluginPackageDir) 623 if err != nil { 624 fmt.Println("Unable to get plugin info: " + err.Error()) 625 } 626 generateHooksGlue(removeExcluded(forRPC)) 627 628 // Generate plugin timer layers 629 log.Println("Generating plugin timer glue") 630 forPlugins, err := getPluginInfo(pluginPackageDir) 631 if err != nil { 632 fmt.Println("Unable to get plugin info: " + err.Error()) 633 } 634 generatePluginTimerLayer(forPlugins) 635 }