mirror of
https://github.com/mattermost/mattermost.git
synced 2026-02-03 20:40:00 -05:00
- Create api.proto with PluginAPI service definition covering all 236 methods from server/public/plugin/api.go - Create api_user_team.proto with User, Session, and Team request/response messages with placeholder fields - Create api_channel_post.proto with Channel, Post, and Emoji messages - Create api_kv_config.proto with KV store, config, plugin, and logging messages - Create api_file_bot.proto with File, Upload, and Bot messages - Create api_remaining.proto with Server, Command, Preference, OAuth, Group, SharedChannel, Property, and Audit messages - Add ViewUsersRestrictions to common.proto - Add apiverify tool that parses Go API interface and proto service to ensure parity - Update Makefile with new proto file mappings Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
231 lines
5.7 KiB
Go
231 lines
5.7 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
// Package main provides the apiverify tool which ensures that the gRPC PluginAPI
|
|
// service definition stays in sync with the Go plugin.API interface.
|
|
//
|
|
// Usage:
|
|
//
|
|
// go run ./server/public/pluginapi/grpc/cmd/apiverify
|
|
//
|
|
// Exit codes:
|
|
// - 0: All methods match
|
|
// - 1: Mismatch detected (missing or extra RPCs)
|
|
// - 2: Error parsing files
|
|
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
func main() {
|
|
// Find the repository root by looking for go.mod
|
|
repoRoot, err := findRepoRoot()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error finding repository root: %v\n", err)
|
|
os.Exit(2)
|
|
}
|
|
|
|
apiGoPath := filepath.Join(repoRoot, "server/public/plugin/api.go")
|
|
apiProtoPath := filepath.Join(repoRoot, "server/public/pluginapi/grpc/proto/api.proto")
|
|
|
|
// Parse Go API interface
|
|
goMethods, err := parseGoAPIInterface(apiGoPath)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error parsing Go API interface: %v\n", err)
|
|
os.Exit(2)
|
|
}
|
|
|
|
// Parse proto service RPCs
|
|
protoRPCs, err := parseProtoServiceRPCs(apiProtoPath)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error parsing proto service RPCs: %v\n", err)
|
|
os.Exit(2)
|
|
}
|
|
|
|
// Compare
|
|
missingInProto := difference(goMethods, protoRPCs)
|
|
extraInProto := difference(protoRPCs, goMethods)
|
|
|
|
if len(missingInProto) == 0 && len(extraInProto) == 0 {
|
|
fmt.Printf("OK: All %d API methods have corresponding RPCs in api.proto\n", len(goMethods))
|
|
os.Exit(0)
|
|
}
|
|
|
|
if len(missingInProto) > 0 {
|
|
fmt.Printf("MISSING in api.proto (%d methods):\n", len(missingInProto))
|
|
sort.Strings(missingInProto)
|
|
for _, m := range missingInProto {
|
|
fmt.Printf(" - %s\n", m)
|
|
}
|
|
}
|
|
|
|
if len(extraInProto) > 0 {
|
|
fmt.Printf("EXTRA in api.proto (not in plugin.API interface, %d RPCs):\n", len(extraInProto))
|
|
sort.Strings(extraInProto)
|
|
for _, m := range extraInProto {
|
|
fmt.Printf(" + %s\n", m)
|
|
}
|
|
}
|
|
|
|
fmt.Printf("\nSummary: Go API has %d methods, proto has %d RPCs\n", len(goMethods), len(protoRPCs))
|
|
os.Exit(1)
|
|
}
|
|
|
|
// findRepoRoot walks up from the current directory looking for the server/public/plugin/api.go file
|
|
// This handles both running from the repo root and from the server subdirectory.
|
|
func findRepoRoot() (string, error) {
|
|
dir, err := os.Getwd()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
for {
|
|
// Check if server/public/plugin/api.go exists (we're at repo root)
|
|
if _, err := os.Stat(filepath.Join(dir, "server/public/plugin/api.go")); err == nil {
|
|
return dir, nil
|
|
}
|
|
// Check if public/plugin/api.go exists (we're in server directory)
|
|
if _, err := os.Stat(filepath.Join(dir, "public/plugin/api.go")); err == nil {
|
|
// Return parent to normalize paths
|
|
return filepath.Dir(dir), nil
|
|
}
|
|
|
|
parent := filepath.Dir(dir)
|
|
if parent == dir {
|
|
// Reached filesystem root without finding the file
|
|
return "", fmt.Errorf("could not find plugin/api.go in any parent directory")
|
|
}
|
|
dir = parent
|
|
}
|
|
}
|
|
|
|
// parseGoAPIInterface extracts method names from the API interface in api.go
|
|
func parseGoAPIInterface(path string) ([]string, error) {
|
|
fset := token.NewFileSet()
|
|
node, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse %s: %w", path, err)
|
|
}
|
|
|
|
var methods []string
|
|
|
|
ast.Inspect(node, func(n ast.Node) bool {
|
|
// Look for type declarations
|
|
typeSpec, ok := n.(*ast.TypeSpec)
|
|
if !ok {
|
|
return true
|
|
}
|
|
|
|
// Check if this is the API interface
|
|
if typeSpec.Name.Name != "API" {
|
|
return true
|
|
}
|
|
|
|
// Get the interface type
|
|
iface, ok := typeSpec.Type.(*ast.InterfaceType)
|
|
if !ok {
|
|
return true
|
|
}
|
|
|
|
// Extract method names
|
|
for _, method := range iface.Methods.List {
|
|
// Each method has exactly one name
|
|
if len(method.Names) == 1 {
|
|
methods = append(methods, method.Names[0].Name)
|
|
}
|
|
}
|
|
|
|
return false // Found the API interface, no need to continue
|
|
})
|
|
|
|
if len(methods) == 0 {
|
|
return nil, fmt.Errorf("no methods found in API interface in %s", path)
|
|
}
|
|
|
|
return methods, nil
|
|
}
|
|
|
|
// parseProtoServiceRPCs extracts RPC names from the PluginAPI service in api.proto
|
|
func parseProtoServiceRPCs(path string) ([]string, error) {
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open %s: %w", path, err)
|
|
}
|
|
defer file.Close()
|
|
|
|
var rpcs []string
|
|
inService := false
|
|
|
|
// Match "service PluginAPI {"
|
|
serviceStartRe := regexp.MustCompile(`^\s*service\s+PluginAPI\s*\{`)
|
|
// Match "rpc MethodName(RequestType) returns (ResponseType);"
|
|
rpcRe := regexp.MustCompile(`^\s*rpc\s+(\w+)\s*\(`)
|
|
// Match closing brace
|
|
closeBraceRe := regexp.MustCompile(`^\s*\}`)
|
|
|
|
braceCount := 0
|
|
scanner := bufio.NewScanner(file)
|
|
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
|
|
if !inService {
|
|
if serviceStartRe.MatchString(line) {
|
|
inService = true
|
|
braceCount = 1
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Track nested braces
|
|
braceCount += strings.Count(line, "{")
|
|
braceCount -= strings.Count(line, "}")
|
|
|
|
if braceCount <= 0 {
|
|
break // End of service definition
|
|
}
|
|
|
|
// Look for RPC definitions
|
|
if matches := rpcRe.FindStringSubmatch(line); matches != nil {
|
|
rpcs = append(rpcs, matches[1])
|
|
}
|
|
|
|
// Also handle closing brace on its own line
|
|
if closeBraceRe.MatchString(line) && braceCount <= 0 {
|
|
break
|
|
}
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, fmt.Errorf("error reading %s: %w", path, err)
|
|
}
|
|
|
|
return rpcs, nil
|
|
}
|
|
|
|
// difference returns elements in a that are not in b
|
|
func difference(a, b []string) []string {
|
|
bSet := make(map[string]bool)
|
|
for _, x := range b {
|
|
bSet[x] = true
|
|
}
|
|
|
|
var diff []string
|
|
for _, x := range a {
|
|
if !bSet[x] {
|
|
diff = append(diff, x)
|
|
}
|
|
}
|
|
|
|
return diff
|
|
}
|