diff --git a/pkg/plugins/yaegi/loader.go b/pkg/plugins/yaegi/loader.go new file mode 100644 index 000000000..9eb5aeaae --- /dev/null +++ b/pkg/plugins/yaegi/loader.go @@ -0,0 +1,118 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package yaegi + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "code.vikunja.io/api/pkg/plugins" + "code.vikunja.io/api/pkg/yaegi_symbols" + + "github.com/traefik/yaegi/interp" + "github.com/traefik/yaegi/stdlib" +) + +func init() { + plugins.YaegiPluginLoader = LoadPluginFull +} + +// LoadPlugin loads a plugin from a directory of Go source files using the Yaegi interpreter. +func LoadPlugin(dir string) (plugins.Plugin, error) { + loaded, err := LoadPluginFull(dir) + if err != nil { + return nil, err + } + return loaded.Plugin, nil +} + +// LoadPluginFull loads a plugin and all its optional capabilities via typed factory functions. +// +// Because Yaegi wraps interpreted values per return type, sub-interface type assertions +// (e.g. Plugin -> AuthenticatedRouterPlugin) do not work. Instead, plugins must export +// typed factory functions for each capability they implement: +// +// - NewPlugin() plugins.Plugin (required) +// - NewAuthenticatedRouterPlugin() plugins.AuthenticatedRouterPlugin (optional) +// - NewUnauthenticatedRouterPlugin() plugins.UnauthenticatedRouterPlugin (optional) +// - NewMigrationPlugin() plugins.MigrationPlugin (optional) +func LoadPluginFull(dir string) (*plugins.LoadedYaegiPlugin, error) { + i := interp.New(interp.Options{}) + if err := i.Use(stdlib.Symbols); err != nil { + return nil, fmt.Errorf("loading stdlib symbols: %w", err) + } + if err := i.Use(yaegi_symbols.Symbols); err != nil { + return nil, fmt.Errorf("loading vikunja symbols: %w", err) + } + + // Read and evaluate all .go files in the plugin directory. + entries, err := os.ReadDir(dir) + if err != nil { + return nil, fmt.Errorf("reading plugin dir %s: %w", dir, err) + } + + for _, e := range entries { + if e.IsDir() || !strings.HasSuffix(e.Name(), ".go") { + continue + } + src, err := os.ReadFile(filepath.Join(dir, e.Name())) + if err != nil { + return nil, fmt.Errorf("reading %s: %w", e.Name(), err) + } + if _, err = i.Eval(string(src)); err != nil { + return nil, fmt.Errorf("evaluating %s: %w", e.Name(), err) + } + } + + loaded := &plugins.LoadedYaegiPlugin{} + + // Required: NewPlugin + v, err := i.Eval("main.NewPlugin") + if err != nil { + return nil, fmt.Errorf("looking up NewPlugin: %w", err) + } + newPlugin, ok := v.Interface().(func() plugins.Plugin) + if !ok { + return nil, fmt.Errorf("NewPlugin has wrong signature: %T", v.Interface()) + } + loaded.Plugin = newPlugin() + + // Optional: NewAuthenticatedRouterPlugin + if v, err := i.Eval("main.NewAuthenticatedRouterPlugin"); err == nil { + if fn, ok := v.Interface().(func() plugins.AuthenticatedRouterPlugin); ok { + loaded.AuthRouter = fn() + } + } + + // Optional: NewUnauthenticatedRouterPlugin + if v, err := i.Eval("main.NewUnauthenticatedRouterPlugin"); err == nil { + if fn, ok := v.Interface().(func() plugins.UnauthenticatedRouterPlugin); ok { + loaded.UnauthRouter = fn() + } + } + + // Optional: NewMigrationPlugin + if v, err := i.Eval("main.NewMigrationPlugin"); err == nil { + if fn, ok := v.Interface().(func() plugins.MigrationPlugin); ok { + loaded.Migration = fn() + } + } + + return loaded, nil +}