mirror of
https://github.com/mattermost/mattermost.git
synced 2026-05-25 11:04:55 -04:00
* add WithRPCErr hooks (server-facing/internal only) * zero _returns on RPC failure in WithRPCErr companions Aligns the WithRPCErr template with the HooksRPCErr godoc contract: when g.client.Call returns a transport error, gob may have partially decoded the reply. Reassign _returns to a zero value before destructuring so callers always receive zeroed outputs alongside a non-nil transport error. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * rename HooksRPCErr to HooksWithRPCErr for naming consistency Every related symbol uses the WithRPCErr suffix (MessageHasBeenPostedWithRPCErr, RunMultiPluginHookWithRPCErr, RunMultiHookWithRPCErr, etc.). Aligning the interface name removes the only outlier and makes the convention uniform. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * rename rpcErrImpl to hooksWithRPCErrImpl Mirrors the existing hooksImpl/Hooks naming pattern on hooksTimerLayer. * add supervisor.HooksWithRPCErr() and drop runtime type assertion The old path did rp.supervisor.Hooks().(HooksWithRPCErr) and handled the "doesn't implement" branch — but that branch was structurally unreachable (the compile-time `_ HooksWithRPCErr = (*hooksTimerLayer)(nil)` assertion guards it). Change supervisor.hooks from `Hooks` to the concrete `*hooksTimerLayer` (which implements both interfaces, enforced at field assignment), add a parallel HooksWithRPCErr() accessor, and call it directly. Hooks() keeps its public Hooks-interface signature via implicit conversion at return. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * drop "implemented by" clause from HooksWithRPCErr godoc Both hooksRPCClient and hooksTimerLayer satisfy the interface, and naming implementations in interface godocs adds rot — the contract is what readers need, not the list of wrappers. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
755 lines
22 KiB
Go
755 lines
22 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/printer"
|
|
"go/token"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"text/template"
|
|
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/tools/imports"
|
|
)
|
|
|
|
var excludedPluginHooks = []string{
|
|
"ChannelMemberWillBeAdded",
|
|
"FileWillBeUploaded",
|
|
"TeamMemberWillBeAdded",
|
|
"Implemented",
|
|
"LoadPluginConfiguration",
|
|
"InstallPlugin",
|
|
"LogAuditRec",
|
|
"LogAuditRecWithLevel",
|
|
"LogDebug",
|
|
"LogError",
|
|
"LogInfo",
|
|
"LogWarn",
|
|
"MessageWillBePosted",
|
|
"MessageWillBeUpdated",
|
|
"MessagesWillBeConsumed",
|
|
"OnActivate",
|
|
"PluginHTTP",
|
|
"ServeHTTP",
|
|
"UploadData",
|
|
"ReceiveSharedChannelAttachmentSyncMsg",
|
|
"ServeMetrics",
|
|
}
|
|
|
|
type IHookEntry struct {
|
|
FuncName string
|
|
Args *ast.FieldList
|
|
Results *ast.FieldList
|
|
}
|
|
|
|
type PluginInterfaceInfo struct {
|
|
Hooks []IHookEntry
|
|
API []IHookEntry
|
|
FileSet *token.FileSet
|
|
}
|
|
|
|
func FieldListToFuncList(fieldList *ast.FieldList, fileset *token.FileSet) string {
|
|
result := []string{}
|
|
if fieldList == nil || len(fieldList.List) == 0 {
|
|
return "()"
|
|
}
|
|
for _, field := range fieldList.List {
|
|
typeNameBuffer := &bytes.Buffer{}
|
|
err := printer.Fprint(typeNameBuffer, fileset, field.Type)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
typeName := typeNameBuffer.String()
|
|
names := []string{}
|
|
for _, name := range field.Names {
|
|
names = append(names, name.Name)
|
|
}
|
|
result = append(result, strings.Join(names, ", ")+" "+typeName)
|
|
}
|
|
|
|
return "(" + strings.Join(result, ", ") + ")"
|
|
}
|
|
|
|
// FieldListToFuncListAppendErr renders the return signature with an extra trailing
|
|
// `error` (the RPC transport error). Used to emit *WithRPCErr companion signatures.
|
|
func FieldListToFuncListAppendErr(fieldList *ast.FieldList, fileset *token.FileSet) string {
|
|
if fieldList == nil || len(fieldList.List) == 0 {
|
|
return "error"
|
|
}
|
|
base := FieldListToFuncList(fieldList, fileset)
|
|
return "(" + base[1:len(base)-1] + ", error)"
|
|
}
|
|
|
|
func FieldListToNames(fieldList *ast.FieldList, variadicForm bool) string {
|
|
result := []string{}
|
|
if fieldList == nil || len(fieldList.List) == 0 {
|
|
return ""
|
|
}
|
|
for _, field := range fieldList.List {
|
|
for _, name := range field.Names {
|
|
paramName := name.Name
|
|
if _, ok := field.Type.(*ast.Ellipsis); ok && variadicForm {
|
|
paramName = fmt.Sprintf("%s...", paramName)
|
|
}
|
|
result = append(result, paramName)
|
|
}
|
|
}
|
|
|
|
return strings.Join(result, ", ")
|
|
}
|
|
|
|
func FieldListToEncodedErrors(structPrefix string, fieldList *ast.FieldList, fileset *token.FileSet) string {
|
|
result := []string{}
|
|
if fieldList == nil {
|
|
return ""
|
|
}
|
|
|
|
nextLetter := 'A'
|
|
for _, field := range fieldList.List {
|
|
typeNameBuffer := &bytes.Buffer{}
|
|
err := printer.Fprint(typeNameBuffer, fileset, field.Type)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
if typeNameBuffer.String() != "error" {
|
|
nextLetter++
|
|
continue
|
|
}
|
|
|
|
name := ""
|
|
if len(field.Names) == 0 {
|
|
name = string(nextLetter)
|
|
nextLetter++
|
|
} else {
|
|
for range field.Names {
|
|
name += string(nextLetter)
|
|
nextLetter++
|
|
}
|
|
}
|
|
|
|
result = append(result, structPrefix+name+" = encodableError("+structPrefix+name+")")
|
|
}
|
|
|
|
return strings.Join(result, "\n")
|
|
}
|
|
|
|
func FieldListDestruct(structPrefix string, fieldList *ast.FieldList, fileset *token.FileSet) string {
|
|
result := []string{}
|
|
if fieldList == nil || len(fieldList.List) == 0 {
|
|
return ""
|
|
}
|
|
nextLetter := 'A'
|
|
for _, field := range fieldList.List {
|
|
typeNameBuffer := &bytes.Buffer{}
|
|
err := printer.Fprint(typeNameBuffer, fileset, field.Type)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
typeName := typeNameBuffer.String()
|
|
suffix := ""
|
|
if strings.HasPrefix(typeName, "...") {
|
|
suffix = "..."
|
|
}
|
|
if len(field.Names) == 0 {
|
|
result = append(result, structPrefix+string(nextLetter)+suffix)
|
|
nextLetter++
|
|
} else {
|
|
for range field.Names {
|
|
result = append(result, structPrefix+string(nextLetter)+suffix)
|
|
nextLetter++
|
|
}
|
|
}
|
|
}
|
|
|
|
return strings.Join(result, ", ")
|
|
}
|
|
|
|
// FieldListDestructAppendErr is FieldListDestruct with an extra trailing `<prefix>RPCErr`
|
|
// variable name appended — used to bind the transport error in *WithRPCErr templates.
|
|
func FieldListDestructAppendErr(structPrefix string, fieldList *ast.FieldList, fileset *token.FileSet) string {
|
|
base := FieldListDestruct(structPrefix, fieldList, fileset)
|
|
rpcErr := structPrefix + "RPCErr"
|
|
if base == "" {
|
|
return rpcErr
|
|
}
|
|
return base + ", " + rpcErr
|
|
}
|
|
|
|
func FieldListToRecordSuccess(structPrefix string, fieldList *ast.FieldList) string {
|
|
if fieldList == nil || len(fieldList.List) == 0 {
|
|
return "true"
|
|
}
|
|
|
|
result := ""
|
|
nextLetter := 'A'
|
|
for _, field := range fieldList.List {
|
|
typeName := baseTypeName(field.Type)
|
|
if typeName == "error" || typeName == "AppError" {
|
|
result = structPrefix + string(nextLetter)
|
|
break
|
|
}
|
|
nextLetter++
|
|
}
|
|
|
|
if result == "" {
|
|
return "true"
|
|
}
|
|
return fmt.Sprintf("%s == nil", result)
|
|
}
|
|
|
|
// FieldListToRecordSuccessWithRPCErr renders the success-flag expression for a *WithRPCErr
|
|
// timer-layer call. It combines the RPC transport check with the base hook's app-error check
|
|
// (if any), so the metric counts a call as successful only when both the transport and the
|
|
// plugin's application-level return signal success.
|
|
func FieldListToRecordSuccessWithRPCErr(structPrefix string, fieldList *ast.FieldList) string {
|
|
rpcErr := structPrefix + "RPCErr == nil"
|
|
base := FieldListToRecordSuccess(structPrefix, fieldList)
|
|
if base == "true" {
|
|
return rpcErr
|
|
}
|
|
return rpcErr + " && " + base
|
|
}
|
|
|
|
func FieldListToStructList(fieldList *ast.FieldList, fileset *token.FileSet) string {
|
|
result := []string{}
|
|
if fieldList == nil || len(fieldList.List) == 0 {
|
|
return ""
|
|
}
|
|
nextLetter := 'A'
|
|
for _, field := range fieldList.List {
|
|
typeNameBuffer := &bytes.Buffer{}
|
|
err := printer.Fprint(typeNameBuffer, fileset, field.Type)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
typeName := typeNameBuffer.String()
|
|
if strings.HasPrefix(typeName, "...") {
|
|
typeName = strings.Replace(typeName, "...", "[]", 1)
|
|
}
|
|
if len(field.Names) == 0 {
|
|
result = append(result, string(nextLetter)+" "+typeName)
|
|
nextLetter++
|
|
} else {
|
|
for range field.Names {
|
|
result = append(result, string(nextLetter)+" "+typeName)
|
|
nextLetter++
|
|
}
|
|
}
|
|
}
|
|
|
|
return strings.Join(result, "\n\t")
|
|
}
|
|
|
|
func baseTypeName(x ast.Expr) string {
|
|
switch t := x.(type) {
|
|
case *ast.Ident:
|
|
return t.Name
|
|
case *ast.SelectorExpr:
|
|
if _, ok := t.X.(*ast.Ident); ok {
|
|
// only possible for qualified type names;
|
|
// assume type is imported
|
|
return t.Sel.Name
|
|
}
|
|
case *ast.ParenExpr:
|
|
return baseTypeName(t.X)
|
|
case *ast.StarExpr:
|
|
return baseTypeName(t.X)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func goList(dir string) ([]string, error) {
|
|
cmd := exec.Command("go", "list", "-f", "{{.Dir}}", dir)
|
|
bytes, err := cmd.Output()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "Can't list packages")
|
|
}
|
|
|
|
return strings.Fields(string(bytes)), nil
|
|
}
|
|
|
|
func (info *PluginInterfaceInfo) addHookMethod(method *ast.Field) {
|
|
info.Hooks = append(info.Hooks, IHookEntry{
|
|
FuncName: method.Names[0].Name,
|
|
Args: method.Type.(*ast.FuncType).Params,
|
|
Results: method.Type.(*ast.FuncType).Results,
|
|
})
|
|
}
|
|
|
|
func (info *PluginInterfaceInfo) addAPIMethod(method *ast.Field) {
|
|
info.API = append(info.API, IHookEntry{
|
|
FuncName: method.Names[0].Name,
|
|
Args: method.Type.(*ast.FuncType).Params,
|
|
Results: method.Type.(*ast.FuncType).Results,
|
|
})
|
|
}
|
|
|
|
func (info *PluginInterfaceInfo) makeHookInspector() func(node ast.Node) bool {
|
|
return func(node ast.Node) bool {
|
|
if typeSpec, ok := node.(*ast.TypeSpec); ok {
|
|
if typeSpec.Name.Name == "Hooks" {
|
|
for _, method := range typeSpec.Type.(*ast.InterfaceType).Methods.List {
|
|
info.addHookMethod(method)
|
|
}
|
|
return false
|
|
} else if typeSpec.Name.Name == "API" {
|
|
for _, method := range typeSpec.Type.(*ast.InterfaceType).Methods.List {
|
|
info.addAPIMethod(method)
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
func getPluginInfo(dir string) (*PluginInterfaceInfo, error) {
|
|
pluginInfo := &PluginInterfaceInfo{
|
|
Hooks: make([]IHookEntry, 0),
|
|
FileSet: token.NewFileSet(),
|
|
}
|
|
|
|
packages, err := parser.ParseDir(pluginInfo.FileSet, dir, nil, parser.ParseComments)
|
|
if err != nil {
|
|
log.Println("Parser error in dir "+dir+": ", err)
|
|
return nil, err
|
|
}
|
|
|
|
for _, pkg := range packages {
|
|
if pkg.Name != "plugin" {
|
|
continue
|
|
}
|
|
|
|
for _, file := range pkg.Files {
|
|
ast.Inspect(file, pluginInfo.makeHookInspector())
|
|
}
|
|
}
|
|
|
|
return pluginInfo, nil
|
|
}
|
|
|
|
var hooksTemplate = `// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
// Code generated by "make pluginapi"
|
|
// DO NOT EDIT
|
|
|
|
package plugin
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
|
|
saml2 "github.com/mattermost/gosaml2"
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
)
|
|
|
|
{{range .HooksMethods}}
|
|
|
|
func init() {
|
|
hookNameToId["{{.Name}}"] = {{.Name}}ID
|
|
}
|
|
|
|
type {{.Name | obscure}}Args struct {
|
|
{{structStyle .Params}}
|
|
}
|
|
|
|
type {{.Name | obscure}}Returns struct {
|
|
{{structStyle .Return}}
|
|
}
|
|
|
|
func (g *hooksRPCClient) {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} {
|
|
_args := &{{.Name | obscure}}Args{ {{valuesOnly .Params}} }
|
|
_returns := &{{.Name | obscure}}Returns{}
|
|
if g.implemented[{{.Name}}ID] {
|
|
if err := g.client.Call("Plugin.{{.Name}}", _args, _returns); err != nil {
|
|
g.log.Error("RPC call {{.Name}} to plugin failed.", mlog.Err(err))
|
|
}
|
|
}
|
|
{{ if .Return }} return {{destruct "_returns." .Return}} {{ end }}
|
|
}
|
|
|
|
// {{.Name}}WithRPCErr returns the same values as {{.Name}}, with an additional trailing error
|
|
// for the RPC transport — always the LAST return slot.
|
|
func (g *hooksRPCClient) {{.Name}}WithRPCErr{{funcStyle .Params}} {{funcStyleAppendErr .Return}} {
|
|
_args := &{{.Name | obscure}}Args{ {{valuesOnly .Params}} }
|
|
_returns := &{{.Name | obscure}}Returns{}
|
|
var _err error
|
|
if g.implemented[{{.Name}}ID] {
|
|
_err = g.client.Call("Plugin.{{.Name}}", _args, _returns)
|
|
if _err != nil {
|
|
// Reset _returns so partial gob decoding can't leak non-zero
|
|
// values past a transport failure (HooksWithRPCErr contract).
|
|
_returns = &{{.Name | obscure}}Returns{}
|
|
g.log.Debug("RPC call {{.Name}} to plugin failed.", mlog.Err(_err))
|
|
}
|
|
}
|
|
{{ if .Return }} return {{destruct "_returns." .Return}}, _err {{ else }} return _err {{ end }}
|
|
}
|
|
|
|
func (s *hooksRPCServer) {{.Name}}(args *{{.Name | obscure}}Args, returns *{{.Name | obscure}}Returns) error {
|
|
if hook, ok := s.impl.(interface {
|
|
{{.Name}}{{funcStyle .Params}} {{funcStyle .Return}}
|
|
}); ok {
|
|
{{if .Return}}{{destruct "returns." .Return}} = {{end}}hook.{{.Name}}({{destruct "args." .Params}})
|
|
{{if .Return}}{{encodeErrors "returns." .Return}}{{end -}}
|
|
} else {
|
|
return encodableError(fmt.Errorf("Hook {{.Name}} called but not implemented."))
|
|
}
|
|
return nil
|
|
}
|
|
{{end}}
|
|
|
|
// HooksWithRPCErr provides a WithRPCErr variant for every generated hook. The last error return
|
|
// is always the RPC transport error — if non-nil, the plugin's other return values are zero. For
|
|
// hooks whose base signature already returns error, the tuple is (originalReturns..., rpcErr)
|
|
// where the final slot is always transport.
|
|
//
|
|
// If the plugin does not implement the hook, the companion returns zero values and a nil error —
|
|
// indistinguishable from a successful invocation that returned zeros. Callers MUST gate on
|
|
// supervisor.Implements(<HookID>) (or use Environment.RunMultiPluginHookWithRPCErr, which gates
|
|
// by the iteration's hook ID — note that any *WithRPCErr method called on the closure's
|
|
// HooksWithRPCErr is independently subject to its own implemented-gate).
|
|
type HooksWithRPCErr interface {
|
|
{{range .HooksMethods}}
|
|
{{.Name}}WithRPCErr{{funcStyle .Params}} {{funcStyleAppendErr .Return}}
|
|
{{end}}
|
|
}
|
|
|
|
{{range .APIMethods}}
|
|
|
|
type {{.Name | obscure}}Args struct {
|
|
{{structStyle .Params}}
|
|
}
|
|
|
|
type {{.Name | obscure}}Returns struct {
|
|
{{structStyle .Return}}
|
|
}
|
|
|
|
func (g *apiRPCClient) {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} {
|
|
_args := &{{.Name | obscure}}Args{ {{valuesOnly .Params}} }
|
|
_returns := &{{.Name | obscure}}Returns{}
|
|
if err := g.client.Call("Plugin.{{.Name}}", _args, _returns); err != nil {
|
|
log.Printf("RPC call to {{.Name}} API failed: %s", err.Error())
|
|
}
|
|
{{ if .Return }} return {{destruct "_returns." .Return}} {{ end }}
|
|
}
|
|
|
|
func (s *apiRPCServer) {{.Name}}(args *{{.Name | obscure}}Args, returns *{{.Name | obscure}}Returns) error {
|
|
if hook, ok := s.impl.(interface {
|
|
{{.Name}}{{funcStyle .Params}} {{funcStyle .Return}}
|
|
}); ok {
|
|
{{if .Return}}{{destruct "returns." .Return}} = {{end}}hook.{{.Name}}({{destruct "args." .Params}})
|
|
{{if .Return}}{{encodeErrors "returns." .Return}}{{end -}}
|
|
} else {
|
|
return encodableError(fmt.Errorf("API {{.Name}} called but not implemented."))
|
|
}
|
|
return nil
|
|
}
|
|
{{end}}
|
|
`
|
|
|
|
var apiTimerLayerTemplate = `// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
// Code generated by "make pluginapi"
|
|
// DO NOT EDIT
|
|
|
|
package plugin
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
timePkg "time"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
)
|
|
|
|
type apiTimerLayer struct {
|
|
pluginID string
|
|
apiImpl API
|
|
metrics metricsInterface
|
|
}
|
|
|
|
func (api *apiTimerLayer) recordTime(startTime timePkg.Time, name string, success bool) {
|
|
if api.metrics != nil {
|
|
elapsedTime := float64(timePkg.Since(startTime)) / float64(timePkg.Second)
|
|
api.metrics.ObservePluginAPIDuration(api.pluginID, name, success, elapsedTime)
|
|
}
|
|
}
|
|
|
|
{{range .APIMethods}}
|
|
|
|
func (api *apiTimerLayer) {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} {
|
|
startTime := timePkg.Now()
|
|
{{ if .Return }} {{destruct "_returns" .Return}} := {{ end }} api.apiImpl.{{.Name}}({{valuesOnly .Params}})
|
|
api.recordTime(startTime, "{{.Name}}", {{ shouldRecordSuccess "_returns" .Return }})
|
|
{{ if .Return }} return {{destruct "_returns" .Return}} {{ end -}}
|
|
}
|
|
|
|
{{end}}
|
|
`
|
|
|
|
var hooksTimerLayerTemplate = `// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
// Code generated by "make pluginapi"
|
|
// DO NOT EDIT
|
|
|
|
package plugin
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
timePkg "time"
|
|
|
|
saml2 "github.com/mattermost/gosaml2"
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
)
|
|
|
|
type hooksTimerLayer struct {
|
|
pluginID string
|
|
hooksImpl Hooks
|
|
hooksWithRPCErrImpl HooksWithRPCErr
|
|
metrics metricsInterface
|
|
}
|
|
|
|
func (hooks *hooksTimerLayer) recordTime(startTime timePkg.Time, name string, success bool) {
|
|
if hooks.metrics != nil {
|
|
elapsedTime := float64(timePkg.Since(startTime)) / float64(timePkg.Second)
|
|
hooks.metrics.ObservePluginHookDuration(hooks.pluginID, name, success, elapsedTime)
|
|
}
|
|
}
|
|
|
|
{{range .HooksMethods}}
|
|
|
|
func (hooks *hooksTimerLayer) {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} {
|
|
startTime := timePkg.Now()
|
|
{{ if .Return }} {{destruct "_returns" .Return}} := {{ end }} hooks.hooksImpl.{{.Name}}({{valuesOnly .Params}})
|
|
hooks.recordTime(startTime, "{{.Name}}", {{ shouldRecordSuccess "_returns" .Return }})
|
|
{{ if .Return }} return {{destruct "_returns" .Return}} {{end -}}
|
|
}
|
|
|
|
{{end}}
|
|
|
|
{{range .HooksMethodsRPCErr}}
|
|
|
|
func (hooks *hooksTimerLayer) {{.Name}}WithRPCErr{{funcStyle .Params}} {{funcStyleAppendErr .Return}} {
|
|
startTime := timePkg.Now()
|
|
{{destructAppendErr "_returns" .Return}} := hooks.hooksWithRPCErrImpl.{{.Name}}WithRPCErr({{valuesOnly .Params}})
|
|
hooks.recordTime(startTime, "{{.Name}}WithRPCErr", {{ shouldRecordSuccessWithRPCErr "_returns" .Return }})
|
|
return {{destructAppendErr "_returns" .Return}}
|
|
}
|
|
|
|
{{end}}
|
|
`
|
|
|
|
type MethodParams struct {
|
|
Name string
|
|
Params *ast.FieldList
|
|
Return *ast.FieldList
|
|
}
|
|
|
|
type HooksTemplateParams struct {
|
|
HooksMethods []MethodParams
|
|
HooksMethodsRPCErr []MethodParams
|
|
APIMethods []MethodParams
|
|
}
|
|
|
|
func generateHooksGlue(info *PluginInterfaceInfo) {
|
|
templateFunctions := map[string]any{
|
|
"funcStyle": func(fields *ast.FieldList) string { return FieldListToFuncList(fields, info.FileSet) },
|
|
"funcStyleAppendErr": func(fields *ast.FieldList) string { return FieldListToFuncListAppendErr(fields, info.FileSet) },
|
|
"structStyle": func(fields *ast.FieldList) string { return FieldListToStructList(fields, info.FileSet) },
|
|
"valuesOnly": func(fields *ast.FieldList) string { return FieldListToNames(fields, false) },
|
|
"encodeErrors": func(structPrefix string, fields *ast.FieldList) string {
|
|
return FieldListToEncodedErrors(structPrefix, fields, info.FileSet)
|
|
},
|
|
"destruct": func(structPrefix string, fields *ast.FieldList) string {
|
|
return FieldListDestruct(structPrefix, fields, info.FileSet)
|
|
},
|
|
"shouldRecordSuccess": func(structPrefix string, fields *ast.FieldList) string {
|
|
return FieldListToRecordSuccess(structPrefix, fields)
|
|
},
|
|
"obscure": func(name string) string {
|
|
return "Z_" + name
|
|
},
|
|
}
|
|
|
|
hooksTemplate, err := template.New("hooks").Funcs(templateFunctions).Parse(hooksTemplate)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
templateParams := HooksTemplateParams{}
|
|
for _, hook := range info.Hooks {
|
|
templateParams.HooksMethods = append(templateParams.HooksMethods, MethodParams{
|
|
Name: hook.FuncName,
|
|
Params: hook.Args,
|
|
Return: hook.Results,
|
|
})
|
|
}
|
|
for _, api := range info.API {
|
|
templateParams.APIMethods = append(templateParams.APIMethods, MethodParams{
|
|
Name: api.FuncName,
|
|
Params: api.Args,
|
|
Return: api.Results,
|
|
})
|
|
}
|
|
templateResult := &bytes.Buffer{}
|
|
err = hooksTemplate.Execute(templateResult, &templateParams)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
formatted, err := imports.Process("", templateResult.Bytes(), nil)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
if err := os.WriteFile(filepath.Join(getPluginPackageDir(), "client_rpc_generated.go"), formatted, 0664); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func generatePluginTimerLayer(info *PluginInterfaceInfo) {
|
|
templateFunctions := map[string]any{
|
|
"funcStyle": func(fields *ast.FieldList) string { return FieldListToFuncList(fields, info.FileSet) },
|
|
"funcStyleAppendErr": func(fields *ast.FieldList) string { return FieldListToFuncListAppendErr(fields, info.FileSet) },
|
|
"structStyle": func(fields *ast.FieldList) string { return FieldListToStructList(fields, info.FileSet) },
|
|
"valuesOnly": func(fields *ast.FieldList) string { return FieldListToNames(fields, true) },
|
|
"destruct": func(structPrefix string, fields *ast.FieldList) string {
|
|
return FieldListDestruct(structPrefix, fields, info.FileSet)
|
|
},
|
|
"destructAppendErr": func(structPrefix string, fields *ast.FieldList) string {
|
|
return FieldListDestructAppendErr(structPrefix, fields, info.FileSet)
|
|
},
|
|
"shouldRecordSuccess": func(structPrefix string, fields *ast.FieldList) string {
|
|
return FieldListToRecordSuccess(structPrefix, fields)
|
|
},
|
|
"shouldRecordSuccessWithRPCErr": func(structPrefix string, fields *ast.FieldList) string {
|
|
return FieldListToRecordSuccessWithRPCErr(structPrefix, fields)
|
|
},
|
|
}
|
|
|
|
// Prepare template params. The timer layer wraps the full Hooks interface, so
|
|
// HooksMethods includes excluded hooks too. *WithRPCErr companions only exist
|
|
// for non-excluded hooks (see HooksWithRPCErr in client_rpc_generated.go), so the
|
|
// excluded subset is filtered into HooksMethodsRPCErr for that loop.
|
|
excluded := func(name string) bool { return slices.Contains(excludedPluginHooks, name) }
|
|
templateParams := HooksTemplateParams{}
|
|
for _, hook := range info.Hooks {
|
|
mp := MethodParams{
|
|
Name: hook.FuncName,
|
|
Params: hook.Args,
|
|
Return: hook.Results,
|
|
}
|
|
templateParams.HooksMethods = append(templateParams.HooksMethods, mp)
|
|
if !excluded(hook.FuncName) {
|
|
templateParams.HooksMethodsRPCErr = append(templateParams.HooksMethodsRPCErr, mp)
|
|
}
|
|
}
|
|
for _, api := range info.API {
|
|
templateParams.APIMethods = append(templateParams.APIMethods, MethodParams{
|
|
Name: api.FuncName,
|
|
Params: api.Args,
|
|
Return: api.Results,
|
|
})
|
|
}
|
|
|
|
pluginTemplates := map[string]string{
|
|
"api_timer_layer_generated.go": apiTimerLayerTemplate,
|
|
"hooks_timer_layer_generated.go": hooksTimerLayerTemplate,
|
|
}
|
|
|
|
for fileName, presetTemplate := range pluginTemplates {
|
|
parsedTemplate, err := template.New("hooks").Funcs(templateFunctions).Parse(presetTemplate)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
templateResult := &bytes.Buffer{}
|
|
err = parsedTemplate.Execute(templateResult, &templateParams)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
formatted, err := imports.Process("", templateResult.Bytes(), nil)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
if err := os.WriteFile(filepath.Join(getPluginPackageDir(), fileName), formatted, 0664); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func getPluginPackageDir() string {
|
|
dirs, err := goList("github.com/mattermost/mattermost/server/public/plugin")
|
|
if err != nil {
|
|
panic(err)
|
|
} else if len(dirs) != 1 {
|
|
panic("More than one package dir, or no dirs!")
|
|
}
|
|
|
|
return dirs[0]
|
|
}
|
|
|
|
func removeExcluded(info *PluginInterfaceInfo, excluded []string) *PluginInterfaceInfo {
|
|
newIface := &PluginInterfaceInfo{
|
|
FileSet: info.FileSet,
|
|
}
|
|
toBeExcluded := func(item string) bool {
|
|
return slices.Contains(excluded, item)
|
|
}
|
|
hooksResult := make([]IHookEntry, 0, len(info.Hooks))
|
|
for _, hook := range info.Hooks {
|
|
if !toBeExcluded(hook.FuncName) {
|
|
hooksResult = append(hooksResult, hook)
|
|
}
|
|
}
|
|
newIface.Hooks = hooksResult
|
|
|
|
apiResult := make([]IHookEntry, 0, len(info.API))
|
|
for _, api := range info.API {
|
|
if !toBeExcluded(api.FuncName) {
|
|
apiResult = append(apiResult, api)
|
|
}
|
|
}
|
|
newIface.API = apiResult
|
|
|
|
return newIface
|
|
}
|
|
|
|
func main() {
|
|
pluginPackageDir := getPluginPackageDir()
|
|
|
|
forRPC, err := getPluginInfo(pluginPackageDir)
|
|
if err != nil {
|
|
fmt.Println("Unable to get plugin info: " + err.Error())
|
|
}
|
|
|
|
log.Println("Generating plugin hooks glue")
|
|
generateHooksGlue(removeExcluded(forRPC, excludedPluginHooks))
|
|
|
|
// Generate plugin timer layers
|
|
log.Println("Generating plugin timer glue")
|
|
forPlugins, err := getPluginInfo(pluginPackageDir)
|
|
if err != nil {
|
|
fmt.Println("Unable to get plugin info: " + err.Error())
|
|
}
|
|
generatePluginTimerLayer(forPlugins)
|
|
}
|