feat(01-02): add optional plugin services and HasPlugin

- App-scoped typed Publish/Lookup with duplicate-provider errors
- Set plugin IDs before Register so HasPlugin sees the full set
- Greeter uses pact.OptionalMessage without importing optional
This commit is contained in:
Jakub Zych
2026-09-16 13:24:30 +02:00
parent 5f2291849d
commit 86969739ae
17 changed files with 297 additions and 12 deletions

View File

@@ -8,6 +8,7 @@ require (
git.golem15.com/golem15/summercms v0.0.0
git.golem15.com/golem15/summercms/examples/hello/plugins/base v0.0.0
git.golem15.com/golem15/summercms/examples/hello/plugins/greeter v0.0.0
git.golem15.com/golem15/summercms/examples/hello/plugins/optional v0.0.0
)
require (
@@ -32,4 +33,5 @@ replace (
git.golem15.com/golem15/summercms => ../..
git.golem15.com/golem15/summercms/examples/hello/plugins/base => ./plugins/base
git.golem15.com/golem15/summercms/examples/hello/plugins/greeter => ./plugins/greeter
git.golem15.com/golem15/summercms/examples/hello/plugins/optional => ./plugins/optional
)

View File

@@ -5,10 +5,12 @@ package main
import (
_ "git.golem15.com/golem15/summercms/examples/hello/plugins/base"
_ "git.golem15.com/golem15/summercms/examples/hello/plugins/greeter"
_ "git.golem15.com/golem15/summercms/examples/hello/plugins/optional"
)
// PluginIDs is the ordered plugin list from summer.yaml.
var PluginIDs = []string{
"golem15.hello",
"golem15.greeter",
"golem15.optional",
}

View File

@@ -5,9 +5,12 @@ import (
"io/fs"
"git.golem15.com/golem15/summercms/backpack"
"git.golem15.com/golem15/summercms/pact"
"git.golem15.com/golem15/summercms/party"
)
var _ pact.HasConfig = (*Plugin)(nil)
//go:embed config
var configFS embed.FS

View File

@@ -5,12 +5,14 @@ import (
"git.golem15.com/golem15/summercms/backpack"
"git.golem15.com/golem15/summercms/bonfire"
"git.golem15.com/golem15/summercms/pact"
"git.golem15.com/golem15/summercms/party"
)
// Plugin is the golem15.greeter plugin. It requires golem15.hello.
type Plugin struct {
app *backpack.App
app *backpack.App
extra string
}
func (p *Plugin) ID() string { return "golem15.greeter" }
@@ -20,6 +22,12 @@ func (p *Plugin) Register(*backpack.App) error { return nil }
func (p *Plugin) Boot(app *backpack.App) error {
p.app = app
p.extra = ""
if app != nil && app.HasPlugin("golem15.optional") {
if msg, ok := app.Lookup[pact.OptionalMessage](); ok {
p.extra = msg.Message()
}
}
return nil
}
@@ -36,12 +44,14 @@ func (p *Plugin) Commands() []bonfire.Command {
posts = p.app.Config.Int("golem15.hello.posts_per_page")
debug = p.app.Config.Bool("app.debug")
}
out.Printf("name=%s posts_per_page=%d debug=%t\n", name, posts, debug)
out.Printf("name=%s posts_per_page=%d debug=%t extra=%s\n", name, posts, debug, p.extra)
return nil
},
}}
}
var _ pact.HasCommands = (*Plugin)(nil)
func init() {
party.Register(&Plugin{})
}

View File

@@ -5,6 +5,7 @@ import (
"context"
"fmt"
"io"
"os"
"strings"
"testing"
@@ -61,6 +62,16 @@ func TestBootUsesPublishedOptionalService(t *testing.T) {
}
}
func TestGreeterSourceDoesNotImportOptional(t *testing.T) {
body, err := os.ReadFile("plugin.go")
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(body), "plugins/optional") {
t.Fatal("greeter must not import the optional plugin module")
}
}
func runHelloCmd(t *testing.T, p *Plugin) string {
t.Helper()
cmds := p.Commands()

View File

@@ -0,0 +1 @@
enabled: true

View File

@@ -0,0 +1,27 @@
module git.golem15.com/golem15/summercms/examples/hello/plugins/optional
go 1.27.0
toolchain go1.27.0
require git.golem15.com/golem15/summercms v0.0.0
require (
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/knadh/koanf/maps v0.1.2 // indirect
github.com/knadh/koanf/parsers/yaml v1.1.1 // indirect
github.com/knadh/koanf/providers/confmap v1.0.1 // indirect
github.com/knadh/koanf/providers/env/v2 v2.0.1 // indirect
github.com/knadh/koanf/providers/file v1.2.1 // indirect
github.com/knadh/koanf/v2 v2.3.6 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/spf13/cobra v1.10.2 // indirect
github.com/spf13/pflag v1.0.9 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/sys v0.32.0 // indirect
)
replace git.golem15.com/golem15/summercms => ../../../..

View File

@@ -0,0 +1,47 @@
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
github.com/knadh/koanf/parsers/yaml v1.1.1 h1:u70vV5IyaM0HvONh8HoqBC97oTgO33KcpZbTLiKVinU=
github.com/knadh/koanf/parsers/yaml v1.1.1/go.mod h1:HHmcHXUrp9cOPcuC+2wrr44GTUB0EC+PyfN3HZD9tFg=
github.com/knadh/koanf/providers/confmap v1.0.1 h1:L15hbvMqlvhwUuCtL9BkL+rqiMAjk6cZc8O9XoDtE3A=
github.com/knadh/koanf/providers/confmap v1.0.1/go.mod h1:txHYHiI2hAtF0/0sCmcuol4IDcuQbKTybiB1nOcUo1A=
github.com/knadh/koanf/providers/env/v2 v2.0.1 h1:a3KagndPqhcWHQv6Pz4OZmwkI/yMeTjkiZye6ZCkyW0=
github.com/knadh/koanf/providers/env/v2 v2.0.1/go.mod h1:1g01PE+Ve1gBfWNNw2wmULRP0tc8RJrjn5p2N/jNCIc=
github.com/knadh/koanf/providers/file v1.2.1 h1:bEWbtQwYrA+W2DtdBrQWyXqJaJSG3KrP3AESOJYp9wM=
github.com/knadh/koanf/providers/file v1.2.1/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA=
github.com/knadh/koanf/v2 v2.3.6 h1:JoQPSJmvS4aP0xNc8xMDr5tcrkSEInL23/Il7pITAKo=
github.com/knadh/koanf/v2 v2.3.6/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -0,0 +1,43 @@
package optional
import (
"embed"
"io/fs"
"git.golem15.com/golem15/summercms/backpack"
"git.golem15.com/golem15/summercms/pact"
"git.golem15.com/golem15/summercms/party"
)
//go:embed config
var configFS embed.FS
// Plugin is the golem15.optional plugin. Greeter may use it without importing this package.
type Plugin struct{}
func (p *Plugin) ID() string { return "golem15.optional" }
func (p *Plugin) Requires() []string { return nil }
func (p *Plugin) Register(app *backpack.App) error {
if app == nil {
return nil
}
return app.Publish[pact.OptionalMessage](extra{})
}
func (p *Plugin) Boot(*backpack.App) error { return nil }
func (p *Plugin) ConfigFS() fs.FS { return configFS }
type extra struct{}
func (extra) Message() string { return "hello-from-optional" }
var (
_ pact.HasConfig = (*Plugin)(nil)
_ pact.OptionalMessage = extra{}
)
func init() {
party.Register(&Plugin{})
}

View File

@@ -5,3 +5,5 @@ plugins:
module: git.golem15.com/golem15/summercms/examples/hello/plugins/base
- id: golem15.greeter
module: git.golem15.com/golem15/summercms/examples/hello/plugins/greeter
- id: golem15.optional
module: git.golem15.com/golem15/summercms/examples/hello/plugins/optional