mirror of https://github.com/jlelse/GoBlog
Basic (experimental) plugin support with two plugin types (exec and middleware)
parent
d813e9579c
commit
2158b156c5
@ -0,0 +1,59 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
|
||||
"github.com/traefik/yaegi/interp"
|
||||
"github.com/traefik/yaegi/stdlib"
|
||||
)
|
||||
|
||||
type plugin struct {
|
||||
Config *PluginConfig
|
||||
plugin reflect.Value
|
||||
}
|
||||
|
||||
// PluginConfig is the configuration of the plugin.
|
||||
type PluginConfig struct {
|
||||
// Path is the storage path of the plugin.
|
||||
Path string
|
||||
// ImportPath is the module path i.e. "github.com/user/module".
|
||||
ImportPath string
|
||||
// PluginType is the type of plugin, this plugin is checked against that type.
|
||||
// The available types are specified by the implementor of this package.
|
||||
PluginType string
|
||||
}
|
||||
|
||||
func (p *plugin) initPlugin(host *PluginHost) error {
|
||||
const errText = "initPlugin: %w"
|
||||
|
||||
interpreter := interp.New(interp.Options{
|
||||
GoPath: p.Config.Path,
|
||||
})
|
||||
|
||||
if err := interpreter.Use(stdlib.Symbols); err != nil {
|
||||
return fmt.Errorf(errText, err)
|
||||
}
|
||||
|
||||
if err := interpreter.Use(host.Symbols); err != nil {
|
||||
return fmt.Errorf(errText, err)
|
||||
}
|
||||
|
||||
if _, err := interpreter.Eval(fmt.Sprintf(`import "%s"`, p.Config.ImportPath)); err != nil {
|
||||
return fmt.Errorf(errText, err)
|
||||
}
|
||||
|
||||
v, err := interpreter.Eval(filepath.Base(p.Config.ImportPath) + ".GetPlugin")
|
||||
if err != nil {
|
||||
return fmt.Errorf(errText, err)
|
||||
}
|
||||
|
||||
result := v.Call([]reflect.Value{})
|
||||
if len(result) > 1 {
|
||||
return fmt.Errorf(errText+": function GetPlugin has more than one return value", ErrValidatingPlugin)
|
||||
}
|
||||
p.plugin = result[0]
|
||||
|
||||
return nil
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/traefik/yaegi/interp"
|
||||
)
|
||||
|
||||
// NewPluginHost initializes a PluginHost.
|
||||
func NewPluginHost(symbols interp.Exports) *PluginHost {
|
||||
return &PluginHost{
|
||||
Plugins: []*plugin{},
|
||||
PluginTypes: map[string]reflect.Type{},
|
||||
Symbols: symbols,
|
||||
}
|
||||
}
|
||||
|
||||
// AddPluginType adds a plugin type to the list.
|
||||
// The interface for the pluginType parameter should be a nil of the plugin type interface:
|
||||
//
|
||||
// (*PluginInterface)(nil)
|
||||
func (h *PluginHost) AddPluginType(name string, pluginType interface{}) {
|
||||
h.PluginTypes[name] = reflect.TypeOf(pluginType).Elem()
|
||||
}
|
||||
|
||||
// LoadPlugin loads a new plugin to the host.
|
||||
func (h *PluginHost) LoadPlugin(config *PluginConfig) (any, error) {
|
||||
p := &plugin{
|
||||
Config: config,
|
||||
}
|
||||
err := p.initPlugin(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = h.validatePlugin(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.Plugins = append(h.Plugins, p)
|
||||
return p.plugin.Interface(), nil
|
||||
}
|
||||
|
||||
func (h *PluginHost) validatePlugin(p *plugin) error {
|
||||
pType := reflect.TypeOf(p.plugin.Interface())
|
||||
|
||||
if _, ok := h.PluginTypes[p.Config.PluginType]; !ok {
|
||||
return fmt.Errorf("validatePlugin: %v: %w", p.Config.PluginType, ErrInvalidType)
|
||||
}
|
||||
|
||||
if !pType.Implements(h.PluginTypes[p.Config.PluginType]) {
|
||||
return fmt.Errorf("validatePlugin:%v: %w %v", p, ErrValidatingPlugin, p.Config.PluginType)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPlugins returns a list of all plugins.
|
||||
func (h *PluginHost) GetPlugins() (list []any) {
|
||||
for _, p := range h.Plugins {
|
||||
list = append(list, p.plugin.Interface())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetPluginsForType returns all the plugins that are of type pluginType or empty if the pluginType doesn't exist.
|
||||
func GetPluginsForType[T any](h *PluginHost, pluginType string) (list []T) {
|
||||
if _, ok := h.PluginTypes[pluginType]; !ok {
|
||||
return
|
||||
}
|
||||
for _, p := range h.Plugins {
|
||||
if p.Config.PluginType != pluginType {
|
||||
continue
|
||||
}
|
||||
if t, ok := p.plugin.Interface().(T); ok {
|
||||
list = append(list, t)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
|
||||
"github.com/traefik/yaegi/interp"
|
||||
)
|
||||
|
||||
// PluginHost manages the plugins.
|
||||
type PluginHost struct {
|
||||
// Plugins contains a list of the plugins.
|
||||
Plugins []*plugin
|
||||
// PluginTypes is a list of plugins types that plugins have to use at least one of.
|
||||
PluginTypes map[string]reflect.Type
|
||||
// Symbols is the map of symbols generated by yaegi extract.
|
||||
Symbols interp.Exports
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrInvalidType is returned when the plugin type specified by the plugin is invalid.
|
||||
ErrInvalidType = errors.New("invalid plugin type")
|
||||
// ErrValidatingPlugin is returned when the plugin fails to fully implement the interface of the plugin type.
|
||||
ErrValidatingPlugin = errors.New("plugin does not implement type")
|
||||
)
|
@ -0,0 +1,51 @@
|
||||
package plugintypes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Interface to GoBlog
|
||||
|
||||
// App is used to access GoBlog's app instance.
|
||||
type App interface {
|
||||
GetDatabase() Database
|
||||
}
|
||||
|
||||
// Database is used to provide access to GoBlog's database.
|
||||
type Database interface {
|
||||
Exec(string, ...any) (sql.Result, error)
|
||||
ExecContext(context.Context, string, ...any) (sql.Result, error)
|
||||
Query(string, ...any) (*sql.Rows, error)
|
||||
QueryContext(context.Context, string, ...any) (*sql.Rows, error)
|
||||
QueryRow(string, ...any) (*sql.Row, error)
|
||||
QueryRowContext(context.Context, string, ...any) (*sql.Row, error)
|
||||
}
|
||||
|
||||
// Plugin types
|
||||
|
||||
// SetApp is used in all plugin types to allow
|
||||
// GoBlog set it's app instance to be accessible by the plugin.
|
||||
type SetApp interface {
|
||||
SetApp(App)
|
||||
}
|
||||
|
||||
// SetConfig is used in all plugin types to allow
|
||||
// GoBlog set plugin configuration.
|
||||
type SetConfig interface {
|
||||
SetConfig(map[string]any)
|
||||
}
|
||||
|
||||
type Exec interface {
|
||||
SetApp
|
||||
SetConfig
|
||||
Exec()
|
||||
}
|
||||
|
||||
type Middleware interface {
|
||||
SetApp
|
||||
SetConfig
|
||||
Handler(http.Handler) http.Handler
|
||||
Prio() int
|
||||
}
|
@ -0,0 +1,153 @@
|
||||
// Code generated by 'yaegi extract go.goblog.app/app/pkgs/plugintypes'. DO NOT EDIT.
|
||||
|
||||
// MIT License
|
||||
//
|
||||
// Copyright (c) 2020 - 2022 Jan-Lukas Else
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
package yaegiwrappers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"go.goblog.app/app/pkgs/plugintypes"
|
||||
"net/http"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Symbols["go.goblog.app/app/pkgs/plugintypes/plugintypes"] = map[string]reflect.Value{
|
||||
// type definitions
|
||||
"App": reflect.ValueOf((*plugintypes.App)(nil)),
|
||||
"Database": reflect.ValueOf((*plugintypes.Database)(nil)),
|
||||
"Exec": reflect.ValueOf((*plugintypes.Exec)(nil)),
|
||||
"Middleware": reflect.ValueOf((*plugintypes.Middleware)(nil)),
|
||||
"SetApp": reflect.ValueOf((*plugintypes.SetApp)(nil)),
|
||||
"SetConfig": reflect.ValueOf((*plugintypes.SetConfig)(nil)),
|
||||
|
||||
// interface wrapper definitions
|
||||
"_App": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_App)(nil)),
|
||||
"_Database": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_Database)(nil)),
|
||||
"_Exec": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_Exec)(nil)),
|
||||
"_Middleware": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_Middleware)(nil)),
|
||||
"_SetApp": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_SetApp)(nil)),
|
||||
"_SetConfig": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_SetConfig)(nil)),
|
||||
}
|
||||
}
|
||||
|
||||
// _go_goblog_app_app_pkgs_plugintypes_App is an interface wrapper for App type
|
||||
type _go_goblog_app_app_pkgs_plugintypes_App struct {
|
||||
IValue interface{}
|
||||
WGetDatabase func() plugintypes.Database
|
||||
}
|
||||
|
||||
func (W _go_goblog_app_app_pkgs_plugintypes_App) GetDatabase() plugintypes.Database {
|
||||
return W.WGetDatabase()
|
||||
}
|
||||
|
||||
// _go_goblog_app_app_pkgs_plugintypes_Database is an interface wrapper for Database type
|
||||
type _go_goblog_app_app_pkgs_plugintypes_Database struct {
|
||||
IValue interface{}
|
||||
WExec func(a0 string, a1 ...any) (sql.Result, error)
|
||||
WExecContext func(a0 context.Context, a1 string, a2 ...any) (sql.Result, error)
|
||||
WQuery func(a0 string, a1 ...any) (*sql.Rows, error)
|
||||
WQueryContext func(a0 context.Context, a1 string, a2 ...any) (*sql.Rows, error)
|
||||
WQueryRow func(a0 string, a1 ...any) (*sql.Row, error)
|
||||
WQueryRowContext func(a0 context.Context, a1 string, a2 ...any) (*sql.Row, error)
|
||||
}
|
||||
|
||||
func (W _go_goblog_app_app_pkgs_plugintypes_Database) Exec(a0 string, a1 ...any) (sql.Result, error) {
|
||||
return W.WExec(a0, a1...)
|
||||
}
|
||||
func (W _go_goblog_app_app_pkgs_plugintypes_Database) ExecContext(a0 context.Context, a1 string, a2 ...any) (sql.Result, error) {
|
||||
return W.WExecContext(a0, a1, a2...)
|
||||
}
|
||||
func (W _go_goblog_app_app_pkgs_plugintypes_Database) Query(a0 string, a1 ...any) (*sql.Rows, error) {
|
||||
return W.WQuery(a0, a1...)
|
||||
}
|
||||
func (W _go_goblog_app_app_pkgs_plugintypes_Database) QueryContext(a0 context.Context, a1 string, a2 ...any) (*sql.Rows, error) {
|
||||
return W.WQueryContext(a0, a1, a2...)
|
||||
}
|
||||
func (W _go_goblog_app_app_pkgs_plugintypes_Database) QueryRow(a0 string, a1 ...any) (*sql.Row, error) {
|
||||
return W.WQueryRow(a0, a1...)
|
||||
}
|
||||
func (W _go_goblog_app_app_pkgs_plugintypes_Database) QueryRowContext(a0 context.Context, a1 string, a2 ...any) (*sql.Row, error) {
|
||||
return W.WQueryRowContext(a0, a1, a2...)
|
||||
}
|
||||
|
||||
// _go_goblog_app_app_pkgs_plugintypes_Exec is an interface wrapper for Exec type
|
||||
type _go_goblog_app_app_pkgs_plugintypes_Exec struct {
|
||||
IValue interface{}
|
||||
WExec func()
|
||||
WSetApp func(a0 plugintypes.App)
|
||||
WSetConfig func(a0 map[string]any)
|
||||
}
|
||||
|
||||
func (W _go_goblog_app_app_pkgs_plugintypes_Exec) Exec() {
|
||||
W.WExec()
|
||||
}
|
||||
func (W _go_goblog_app_app_pkgs_plugintypes_Exec) SetApp(a0 plugintypes.App) {
|
||||
W.WSetApp(a0)
|
||||
}
|
||||
func (W _go_goblog_app_app_pkgs_plugintypes_Exec) SetConfig(a0 map[string]any) {
|
||||
W.WSetConfig(a0)
|
||||
}
|
||||
|
||||
// _go_goblog_app_app_pkgs_plugintypes_Middleware is an interface wrapper for Middleware type
|
||||
type _go_goblog_app_app_pkgs_plugintypes_Middleware struct {
|
||||
IValue interface{}
|
||||
WHandler func(a0 http.Handler) http.Handler
|
||||
WPrio func() int
|
||||
WSetApp func(a0 plugintypes.App)
|
||||
WSetConfig func(a0 map[string]any)
|
||||
}
|
||||
|
||||
func (W _go_goblog_app_app_pkgs_plugintypes_Middleware) Handler(a0 http.Handler) http.Handler {
|
||||
return W.WHandler(a0)
|
||||
}
|
||||
func (W _go_goblog_app_app_pkgs_plugintypes_Middleware) Prio() int {
|
||||
return W.WPrio()
|
||||
}
|
||||
func (W _go_goblog_app_app_pkgs_plugintypes_Middleware) SetApp(a0 plugintypes.App) {
|
||||
W.WSetApp(a0)
|
||||
}
|
||||
func (W _go_goblog_app_app_pkgs_plugintypes_Middleware) SetConfig(a0 map[string]any) {
|
||||
W.WSetConfig(a0)
|
||||
}
|
||||
|
||||
// _go_goblog_app_app_pkgs_plugintypes_SetApp is an interface wrapper for SetApp type
|
||||
type _go_goblog_app_app_pkgs_plugintypes_SetApp struct {
|
||||
IValue interface{}
|
||||
WSetApp func(a0 plugintypes.App)
|
||||
}
|
||||
|
||||
func (W _go_goblog_app_app_pkgs_plugintypes_SetApp) SetApp(a0 plugintypes.App) {
|
||||
W.WSetApp(a0)
|
||||
}
|
||||
|
||||
// _go_goblog_app_app_pkgs_plugintypes_SetConfig is an interface wrapper for SetConfig type
|
||||
type _go_goblog_app_app_pkgs_plugintypes_SetConfig struct {
|
||||
IValue interface{}
|
||||
WSetConfig func(a0 map[string]any)
|
||||
}
|
||||
|
||||
func (W _go_goblog_app_app_pkgs_plugintypes_SetConfig) SetConfig(a0 map[string]any) {
|
||||
W.WSetConfig(a0)
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package yaegiwrappers
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
var (
|
||||
Symbols = make(map[string]map[string]reflect.Value)
|
||||
)
|
||||
|
||||
//go:generate yaegi extract -license ../../LICENSE -name yaegiwrappers go.goblog.app/app/pkgs/plugintypes
|
@ -0,0 +1,52 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"go.goblog.app/app/pkgs/plugins"
|
||||
"go.goblog.app/app/pkgs/plugintypes"
|
||||
"go.goblog.app/app/pkgs/yaegiwrappers"
|
||||
)
|
||||
|
||||
func (a *goBlog) initPlugins() error {
|
||||
a.pluginHost = plugins.NewPluginHost(yaegiwrappers.Symbols)
|
||||
|
||||
a.pluginHost.AddPluginType("exec", (*plugintypes.Exec)(nil))
|
||||
a.pluginHost.AddPluginType("middleware", (*plugintypes.Middleware)(nil))
|
||||
|
||||
for _, pc := range a.cfg.Plugins {
|
||||
if pluginInterface, err := a.pluginHost.LoadPlugin(&plugins.PluginConfig{
|
||||
Path: pc.Path,
|
||||
ImportPath: pc.Import,
|
||||
PluginType: pc.Type,
|
||||
}); err != nil {
|
||||
return err
|
||||
} else if pluginInterface != nil {
|
||||
if setAppPlugin, ok := pluginInterface.(plugintypes.SetApp); ok {
|
||||
setAppPlugin.SetApp(a)
|
||||
}
|
||||
if setConfigPlugin, ok := pluginInterface.(plugintypes.SetConfig); ok {
|
||||
setConfigPlugin.SetConfig(pc.Config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
execs := getPluginsForType[plugintypes.Exec](a, "exec")
|
||||
for _, p := range execs {
|
||||
go p.Exec()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getPluginsForType[T any](a *goBlog, pluginType string) (list []T) {
|
||||
return plugins.GetPluginsForType[T](a.pluginHost, pluginType)
|
||||
}
|
||||
|
||||
// Implement all needed interfaces for goblog
|
||||
|
||||
var _ plugintypes.App = &goBlog{}
|
||||
|
||||
func (a *goBlog) GetDatabase() plugintypes.Database {
|
||||
return a.db
|
||||
}
|
||||
|
||||
var _ plugintypes.Database = &database{}
|
@ -0,0 +1,37 @@
|
||||
package demoexec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"go.goblog.app/app/pkgs/plugintypes"
|
||||
)
|
||||
|
||||
func GetPlugin() plugintypes.Exec {
|
||||
return &plugin{}
|
||||
}
|
||||
|
||||
type plugin struct {
|
||||
app plugintypes.App
|
||||
}
|
||||
|
||||
func (p *plugin) SetApp(app plugintypes.App) {
|
||||
p.app = app
|
||||
}
|
||||
|
||||
func (*plugin) SetConfig(_ map[string]any) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
func (p *plugin) Exec() {
|
||||
fmt.Println("Hello World from the demo plugin!")
|
||||
|
||||
row, _ := p.app.GetDatabase().QueryRow("select count (*) from posts")
|
||||
var count int
|
||||
if err := row.Scan(&count); err != nil {
|
||||
fmt.Println(fmt.Errorf("failed to count posts: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Number of posts in database: %d", count)
|
||||
fmt.Println()
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package demomiddleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"go.goblog.app/app/pkgs/plugintypes"
|
||||
)
|
||||
|
||||
func GetPlugin() plugintypes.Middleware {
|
||||
return &plugin{}
|
||||
}
|
||||
|
||||
type plugin struct {
|
||||
app plugintypes.App
|
||||
config map[string]any
|
||||
}
|
||||
|
||||
func (p *plugin) SetApp(app plugintypes.App) {
|
||||
p.app = app
|
||||
}
|
||||
|
||||
func (p *plugin) SetConfig(config map[string]any) {
|
||||
p.config = config
|
||||
}
|
||||
|
||||
func (p *plugin) Prio() int {
|
||||
if prioAny, ok := p.config["prio"]; ok {
|
||||
if prio, ok := prioAny.(int); ok {
|
||||
return prio
|
||||
}
|
||||
}
|
||||
return 100
|
||||
}
|
||||
|
||||
func (p *plugin) Handler(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-Demo", fmt.Sprintf("This is from the demo middleware with prio %d", p.Prio()))
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.goblog.app/app/pkgs/plugintypes"
|
||||
)
|
||||
|
||||
func TestExecPlugin(t *testing.T) {
|
||||
app := &goBlog{
|
||||
cfg: createDefaultTestConfig(t),
|
||||
}
|
||||
app.cfg.Plugins = []*configPlugin{
|
||||
{
|
||||
Path: "./plugins/demo",
|
||||
Type: "exec",
|
||||
Import: "demoexec",
|
||||
},
|
||||
}
|
||||
|
||||
err := app.initConfig(false)
|
||||
require.NoError(t, err)
|
||||
err = app.initPlugins()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMiddlewarePlugin(t *testing.T) {
|
||||
app := &goBlog{
|
||||
cfg: createDefaultTestConfig(t),
|
||||
}
|
||||
app.cfg.Plugins = []*configPlugin{
|
||||
{
|
||||
Path: "./plugins/demo",
|
||||
Type: "middleware",
|
||||
Import: "demomiddleware",
|
||||
Config: map[string]any{
|
||||
"prio": 99,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := app.initConfig(false)
|
||||
require.NoError(t, err)
|
||||
err = app.initPlugins()
|
||||
require.NoError(t, err)
|
||||
|
||||
middlewarePlugins := getPluginsForType[plugintypes.Middleware](app, "middleware")
|
||||
if assert.Len(t, middlewarePlugins, 1) {
|
||||
mdw := middlewarePlugins[0]
|
||||
assert.Equal(t, 99, mdw.Prio())
|
||||
}
|
||||
|
||||
}
|