github.com/levb/mattermost-server@v5.3.1+incompatible/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"
    16  	"os/exec"
    17  	"path/filepath"
    18  	"strings"
    19  	"text/template"
    20  
    21  	"github.com/pkg/errors"
    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) 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  			result = append(result, name.Name)
    66  		}
    67  	}
    68  
    69  	return strings.Join(result, ", ")
    70  }
    71  
    72  func FieldListDestruct(structPrefix string, fieldList *ast.FieldList, fileset *token.FileSet) string {
    73  	result := []string{}
    74  	if fieldList == nil || len(fieldList.List) == 0 {
    75  		return ""
    76  	}
    77  	nextLetter := 'A'
    78  	for _, field := range fieldList.List {
    79  		typeNameBuffer := &bytes.Buffer{}
    80  		err := printer.Fprint(typeNameBuffer, fileset, field.Type)
    81  		if err != nil {
    82  			panic(err)
    83  		}
    84  		typeName := typeNameBuffer.String()
    85  		suffix := ""
    86  		if strings.HasPrefix(typeName, "...") {
    87  			suffix = "..."
    88  		}
    89  		if len(field.Names) == 0 {
    90  			result = append(result, structPrefix+string(nextLetter)+suffix)
    91  			nextLetter += 1
    92  		} else {
    93  			for range field.Names {
    94  				result = append(result, structPrefix+string(nextLetter)+suffix)
    95  				nextLetter += 1
    96  			}
    97  		}
    98  	}
    99  
   100  	return strings.Join(result, ", ")
   101  }
   102  
   103  func FieldListToStructList(fieldList *ast.FieldList, fileset *token.FileSet) string {
   104  	result := []string{}
   105  	if fieldList == nil || len(fieldList.List) == 0 {
   106  		return ""
   107  	}
   108  	nextLetter := 'A'
   109  	for _, field := range fieldList.List {
   110  		typeNameBuffer := &bytes.Buffer{}
   111  		err := printer.Fprint(typeNameBuffer, fileset, field.Type)
   112  		if err != nil {
   113  			panic(err)
   114  		}
   115  		typeName := typeNameBuffer.String()
   116  		if strings.HasPrefix(typeName, "...") {
   117  			typeName = strings.Replace(typeName, "...", "[]", 1)
   118  		}
   119  		if len(field.Names) == 0 {
   120  			result = append(result, string(nextLetter)+" "+typeName)
   121  			nextLetter += 1
   122  		} else {
   123  			for range field.Names {
   124  				result = append(result, string(nextLetter)+" "+typeName)
   125  				nextLetter += 1
   126  			}
   127  		}
   128  	}
   129  
   130  	return strings.Join(result, "\n\t")
   131  }
   132  
   133  func goList(dir string) ([]string, error) {
   134  	cmd := exec.Command("go", "list", "-f", "{{.Dir}}", dir)
   135  	bytes, err := cmd.Output()
   136  	if err != nil {
   137  		return nil, errors.Wrap(err, "Can't list packages")
   138  	}
   139  
   140  	return strings.Fields(string(bytes)), nil
   141  }
   142  
   143  func (info *PluginInterfaceInfo) addHookMethod(method *ast.Field) {
   144  	info.Hooks = append(info.Hooks, IHookEntry{
   145  		FuncName: method.Names[0].Name,
   146  		Args:     method.Type.(*ast.FuncType).Params,
   147  		Results:  method.Type.(*ast.FuncType).Results,
   148  	})
   149  }
   150  
   151  func (info *PluginInterfaceInfo) addAPIMethod(method *ast.Field) {
   152  	info.API = append(info.API, IHookEntry{
   153  		FuncName: method.Names[0].Name,
   154  		Args:     method.Type.(*ast.FuncType).Params,
   155  		Results:  method.Type.(*ast.FuncType).Results,
   156  	})
   157  }
   158  
   159  func (info *PluginInterfaceInfo) makeHookInspector() func(node ast.Node) bool {
   160  	return func(node ast.Node) bool {
   161  		if typeSpec, ok := node.(*ast.TypeSpec); ok {
   162  			if typeSpec.Name.Name == "Hooks" {
   163  				for _, method := range typeSpec.Type.(*ast.InterfaceType).Methods.List {
   164  					info.addHookMethod(method)
   165  				}
   166  				return false
   167  			} else if typeSpec.Name.Name == "API" {
   168  				for _, method := range typeSpec.Type.(*ast.InterfaceType).Methods.List {
   169  					info.addAPIMethod(method)
   170  				}
   171  				return false
   172  			}
   173  		}
   174  		return true
   175  	}
   176  }
   177  
   178  func getPluginInfo(dir string) (*PluginInterfaceInfo, error) {
   179  	pluginInfo := &PluginInterfaceInfo{
   180  		Hooks:   make([]IHookEntry, 0),
   181  		FileSet: token.NewFileSet(),
   182  	}
   183  
   184  	packages, err := parser.ParseDir(pluginInfo.FileSet, dir, nil, parser.ParseComments)
   185  	if err != nil {
   186  		log.Println("Parser error in dir "+dir+": ", err)
   187  	}
   188  
   189  	for _, pkg := range packages {
   190  		if pkg.Name != "plugin" {
   191  			continue
   192  		}
   193  
   194  		for _, file := range pkg.Files {
   195  			ast.Inspect(file, pluginInfo.makeHookInspector())
   196  		}
   197  	}
   198  
   199  	return pluginInfo, nil
   200  }
   201  
   202  var hooksTemplate = `// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
   203  // See LICENSE.txt for license information.
   204  
   205  // Code generated by "make pluginapi"
   206  // DO NOT EDIT
   207  
   208  package plugin
   209  
   210  {{range .HooksMethods}}
   211  
   212  func init() {
   213  	hookNameToId["{{.Name}}"] = {{.Name}}Id
   214  }
   215  
   216  type {{.Name | obscure}}Args struct {
   217  	{{structStyle .Params}}
   218  }
   219  
   220  type {{.Name | obscure}}Returns struct {
   221  	{{structStyle .Return}}
   222  }
   223  
   224  func (g *hooksRPCClient) {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} {
   225  	_args := &{{.Name | obscure}}Args{ {{valuesOnly .Params}} }
   226  	_returns := &{{.Name | obscure}}Returns{}
   227  	if g.implemented[{{.Name}}Id] {
   228  		if err := g.client.Call("Plugin.{{.Name}}", _args, _returns); err != nil {
   229  			g.log.Error("RPC call {{.Name}} to plugin failed.", mlog.Err(err))
   230  		}
   231  	}
   232  	return {{destruct "_returns." .Return}}
   233  }
   234  
   235  func (s *hooksRPCServer) {{.Name}}(args *{{.Name | obscure}}Args, returns *{{.Name | obscure}}Returns) error {
   236  	if hook, ok := s.impl.(interface {
   237  		{{.Name}}{{funcStyle .Params}} {{funcStyle .Return}}
   238  	}); ok {
   239  		{{if .Return}}{{destruct "returns." .Return}} = {{end}}hook.{{.Name}}({{destruct "args." .Params}})
   240  	} else {
   241  		return fmt.Errorf("Hook {{.Name}} called but not implemented.")
   242  	}
   243  	return nil
   244  }
   245  {{end}}
   246  
   247  {{range .APIMethods}}
   248  
   249  type {{.Name | obscure}}Args struct {
   250  	{{structStyle .Params}}
   251  }
   252  
   253  type {{.Name | obscure}}Returns struct {
   254  	{{structStyle .Return}}
   255  }
   256  
   257  func (g *apiRPCClient) {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} {
   258  	_args := &{{.Name | obscure}}Args{ {{valuesOnly .Params}} }
   259  	_returns := &{{.Name | obscure}}Returns{}
   260  	if err := g.client.Call("Plugin.{{.Name}}", _args, _returns); err != nil {
   261  		log.Printf("RPC call to {{.Name}} API failed: %s", err.Error())
   262  	}
   263  	return {{destruct "_returns." .Return}}
   264  }
   265  
   266  func (s *apiRPCServer) {{.Name}}(args *{{.Name | obscure}}Args, returns *{{.Name | obscure}}Returns) error {
   267  	if hook, ok := s.impl.(interface {
   268  		{{.Name}}{{funcStyle .Params}} {{funcStyle .Return}}
   269  	}); ok {
   270  		{{if .Return}}{{destruct "returns." .Return}} = {{end}}hook.{{.Name}}({{destruct "args." .Params}})
   271  	} else {
   272  		return fmt.Errorf("API {{.Name}} called but not implemented.")
   273  	}
   274  	return nil
   275  }
   276  {{end}}
   277  `
   278  
   279  type MethodParams struct {
   280  	Name   string
   281  	Params *ast.FieldList
   282  	Return *ast.FieldList
   283  }
   284  
   285  type HooksTemplateParams struct {
   286  	HooksMethods []MethodParams
   287  	APIMethods   []MethodParams
   288  }
   289  
   290  func generateGlue(info *PluginInterfaceInfo) {
   291  	templateFunctions := map[string]interface{}{
   292  		"funcStyle":   func(fields *ast.FieldList) string { return FieldListToFuncList(fields, info.FileSet) },
   293  		"structStyle": func(fields *ast.FieldList) string { return FieldListToStructList(fields, info.FileSet) },
   294  		"valuesOnly":  func(fields *ast.FieldList) string { return FieldListToNames(fields, info.FileSet) },
   295  		"destruct": func(structPrefix string, fields *ast.FieldList) string {
   296  			return FieldListDestruct(structPrefix, fields, info.FileSet)
   297  		},
   298  		"obscure": func(name string) string {
   299  			return "Z_" + name
   300  		},
   301  	}
   302  
   303  	hooksTemplate, err := template.New("hooks").Funcs(templateFunctions).Parse(hooksTemplate)
   304  	if err != nil {
   305  		panic(err)
   306  	}
   307  
   308  	templateParams := HooksTemplateParams{}
   309  	for _, hook := range info.Hooks {
   310  		templateParams.HooksMethods = append(templateParams.HooksMethods, MethodParams{
   311  			Name:   hook.FuncName,
   312  			Params: hook.Args,
   313  			Return: hook.Results,
   314  		})
   315  	}
   316  	for _, api := range info.API {
   317  		templateParams.APIMethods = append(templateParams.APIMethods, MethodParams{
   318  			Name:   api.FuncName,
   319  			Params: api.Args,
   320  			Return: api.Results,
   321  		})
   322  	}
   323  	templateResult := &bytes.Buffer{}
   324  	hooksTemplate.Execute(templateResult, &templateParams)
   325  
   326  	importsBuffer := &bytes.Buffer{}
   327  	cmd := exec.Command("goimports")
   328  	cmd.Stdin = templateResult
   329  	cmd.Stdout = importsBuffer
   330  	cmd.Stderr = os.Stderr
   331  	if err := cmd.Run(); err != nil {
   332  		panic(err)
   333  	}
   334  
   335  	if err := ioutil.WriteFile(filepath.Join(getPluginPackageDir(), "client_rpc_generated.go"), importsBuffer.Bytes(), 0664); err != nil {
   336  		panic(err)
   337  	}
   338  }
   339  
   340  func getPluginPackageDir() string {
   341  	dirs, err := goList("github.com/mattermost/mattermost-server/plugin")
   342  	if err != nil {
   343  		panic(err)
   344  	} else if len(dirs) != 1 {
   345  		panic("More than one package dir, or no dirs!")
   346  	}
   347  
   348  	return dirs[0]
   349  }
   350  
   351  func removeExcluded(info *PluginInterfaceInfo) *PluginInterfaceInfo {
   352  	toBeExcluded := func(item string) bool {
   353  		excluded := []string{
   354  			"OnActivate",
   355  			"Implemented",
   356  			"LoadPluginConfiguration",
   357  			"ServeHTTP",
   358  			"FileWillBeUploaded",
   359  		}
   360  		for _, exclusion := range excluded {
   361  			if exclusion == item {
   362  				return true
   363  			}
   364  		}
   365  		return false
   366  	}
   367  	hooksResult := make([]IHookEntry, 0, len(info.Hooks))
   368  	for _, hook := range info.Hooks {
   369  		if !toBeExcluded(hook.FuncName) {
   370  			hooksResult = append(hooksResult, hook)
   371  		}
   372  	}
   373  	info.Hooks = hooksResult
   374  
   375  	apiResult := make([]IHookEntry, 0, len(info.API))
   376  	for _, api := range info.API {
   377  		if !toBeExcluded(api.FuncName) {
   378  			apiResult = append(apiResult, api)
   379  		}
   380  	}
   381  	info.API = apiResult
   382  
   383  	return info
   384  }
   385  
   386  func main() {
   387  	pluginPackageDir := getPluginPackageDir()
   388  
   389  	log.Println("Generating plugin glue")
   390  	info, err := getPluginInfo(pluginPackageDir)
   391  	if err != nil {
   392  		fmt.Println("Unable to get plugin info: " + err.Error())
   393  	}
   394  
   395  	info = removeExcluded(info)
   396  
   397  	generateGlue(info)
   398  }