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  }