feat(01-01): boot hello app and dispatch greeter:hello
- Add party plugin registry with Register-before-Boot activation - Add backpack, compass, bonfire, and pact capability interfaces - Add examples/hello with two compiled plugin modules
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
# Go
|
# Go
|
||||||
/bin/
|
/bin/
|
||||||
|
/examples/hello/bin/
|
||||||
/dist/
|
/dist/
|
||||||
*.test
|
*.test
|
||||||
*.out
|
*.out
|
||||||
|
|||||||
13
backpack/app.go
Normal file
13
backpack/app.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package backpack
|
||||||
|
|
||||||
|
import "git.golem15.com/golem15/summercms/compass"
|
||||||
|
|
||||||
|
// App is the per-instance application container. It must not import party.
|
||||||
|
type App struct {
|
||||||
|
Config *compass.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns an App that owns cfg.
|
||||||
|
func New(cfg *compass.Config) *App {
|
||||||
|
return &App{Config: cfg}
|
||||||
|
}
|
||||||
37
backpack/app_test.go
Normal file
37
backpack/app_test.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package backpack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go/parser"
|
||||||
|
"go/token"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDoesNotImportParty(t *testing.T) {
|
||||||
|
entries, err := os.ReadDir(".")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
fset := token.NewFileSet()
|
||||||
|
for _, entry := range entries {
|
||||||
|
name := entry.Name()
|
||||||
|
if entry.IsDir() || !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
file, err := parser.ParseFile(fset, name, nil, parser.ImportsOnly)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, spec := range file.Imports {
|
||||||
|
path, err := strconv.Unquote(spec.Path.Value)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if path == "git.golem15.com/golem15/summercms/party" {
|
||||||
|
t.Fatalf("%s imports party", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
110
bonfire/command.go
Normal file
110
bonfire/command.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package bonfire
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Command is a console command value collected from plugins or the tool.
|
||||||
|
type Command struct {
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
Flags []Flag
|
||||||
|
Args []Arg
|
||||||
|
Run func(ctx context.Context, in Input, out Output) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flag describes a string flag passed to a command.
|
||||||
|
type Flag struct {
|
||||||
|
Name string
|
||||||
|
Shorthand string
|
||||||
|
Description string
|
||||||
|
Default string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arg describes a positional argument.
|
||||||
|
type Arg struct {
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
Required bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input is the parsed argument and flag view handed to Command.Run.
|
||||||
|
type Input interface {
|
||||||
|
Args() []string
|
||||||
|
Argument(name string) (string, bool)
|
||||||
|
Flag(name string) (string, bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output is an injected writer used by commands instead of package-level stdout.
|
||||||
|
type Output interface {
|
||||||
|
io.Writer
|
||||||
|
Println(a ...any)
|
||||||
|
Printf(format string, a ...any)
|
||||||
|
}
|
||||||
|
|
||||||
|
type cobraInput struct {
|
||||||
|
cmd *cobra.Command
|
||||||
|
spec Command
|
||||||
|
args []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (in cobraInput) Args() []string { return in.args }
|
||||||
|
|
||||||
|
func (in cobraInput) Argument(name string) (string, bool) {
|
||||||
|
for i, arg := range in.spec.Args {
|
||||||
|
if arg.Name == name {
|
||||||
|
if i < len(in.args) {
|
||||||
|
return in.args[i], true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (in cobraInput) Flag(name string) (string, bool) {
|
||||||
|
if in.cmd == nil || in.cmd.Flags() == nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
if !in.cmd.Flags().Changed(name) {
|
||||||
|
val, err := in.cmd.Flags().GetString(name)
|
||||||
|
if err != nil || val == "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return val, true
|
||||||
|
}
|
||||||
|
val, err := in.cmd.Flags().GetString(name)
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return val, true
|
||||||
|
}
|
||||||
|
|
||||||
|
type writerOutput struct {
|
||||||
|
w io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o writerOutput) Write(p []byte) (int, error) {
|
||||||
|
if o.w == nil {
|
||||||
|
return 0, io.ErrClosedPipe
|
||||||
|
}
|
||||||
|
return o.w.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o writerOutput) Println(a ...any) {
|
||||||
|
if o.w == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintln(o.w, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o writerOutput) Printf(format string, a ...any) {
|
||||||
|
if o.w == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintf(o.w, format, a...)
|
||||||
|
}
|
||||||
58
bonfire/root.go
Normal file
58
bonfire/root.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package bonfire
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewRoot builds the shared cobra root used by the summer tool and app binaries.
|
||||||
|
// It accepts command values rather than a party registry to avoid an import cycle.
|
||||||
|
func NewRoot(name string, commands []Command, out io.Writer) *cobra.Command {
|
||||||
|
if out == nil {
|
||||||
|
out = os.Stdout
|
||||||
|
}
|
||||||
|
root := &cobra.Command{
|
||||||
|
Use: name,
|
||||||
|
SilenceUsage: true,
|
||||||
|
SilenceErrors: true,
|
||||||
|
CompletionOptions: cobra.CompletionOptions{
|
||||||
|
DisableDefaultCmd: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
root.SetOut(out)
|
||||||
|
root.SetErr(out)
|
||||||
|
for _, command := range commands {
|
||||||
|
root.AddCommand(wrap(command, out))
|
||||||
|
}
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrap(c Command, out io.Writer) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: c.Name,
|
||||||
|
Short: c.Description,
|
||||||
|
Args: cobra.ArbitraryArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if c.Run == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
writer := out
|
||||||
|
if cmd != nil {
|
||||||
|
if w := cmd.OutOrStdout(); w != nil {
|
||||||
|
writer = w
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c.Run(cmd.Context(), cobraInput{cmd: cmd, spec: c, args: args}, writerOutput{w: writer})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, flag := range c.Flags {
|
||||||
|
if flag.Shorthand != "" {
|
||||||
|
cmd.Flags().StringP(flag.Name, flag.Shorthand, flag.Default, flag.Description)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cmd.Flags().String(flag.Name, flag.Default, flag.Description)
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
72
compass/config.go
Normal file
72
compass/config.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package compass
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/knadh/koanf/parsers/yaml"
|
||||||
|
"github.com/knadh/koanf/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config is a YAML configuration tree with dot-path access.
|
||||||
|
type Config struct {
|
||||||
|
k *koanf.Koanf
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load reads config/<section>.yaml files from dir. Each filename stem becomes
|
||||||
|
// the top-level section, so config/app.yaml key name is app.name.
|
||||||
|
func Load(dir string) (*Config, error) {
|
||||||
|
k := koanf.New(".")
|
||||||
|
entries, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("compass: read config dir %s: %w", dir, err)
|
||||||
|
}
|
||||||
|
parser := yaml.Parser()
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := entry.Name()
|
||||||
|
section, ok := sectionName(name)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
path := filepath.Join(dir, name)
|
||||||
|
raw, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("compass: read %s: %w", path, err)
|
||||||
|
}
|
||||||
|
parsed, err := parser.Unmarshal(raw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("compass: parse %s: %w", path, err)
|
||||||
|
}
|
||||||
|
if parsed == nil {
|
||||||
|
parsed = map[string]any{}
|
||||||
|
}
|
||||||
|
if err := k.Set(section, parsed); err != nil {
|
||||||
|
return nil, fmt.Errorf("compass: set section %s: %w", section, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &Config{k: k}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the string value at path, or "" if unset.
|
||||||
|
func (c *Config) String(path string) string {
|
||||||
|
if c == nil || c.k == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return c.k.String(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sectionName(filename string) (string, bool) {
|
||||||
|
switch {
|
||||||
|
case strings.HasSuffix(filename, ".yaml"):
|
||||||
|
return strings.TrimSuffix(filename, ".yaml"), true
|
||||||
|
case strings.HasSuffix(filename, ".yml"):
|
||||||
|
return strings.TrimSuffix(filename, ".yml"), true
|
||||||
|
default:
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
}
|
||||||
23
compass/config_test.go
Normal file
23
compass/config_test.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package compass
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoadStringFromAppSection(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
path := filepath.Join(dir, "app.yaml")
|
||||||
|
if err := os.WriteFile(path, []byte("name: hello-app\n"), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := Load(dir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load: %v", err)
|
||||||
|
}
|
||||||
|
if got := cfg.String("app.name"); got != "hello-app" {
|
||||||
|
t.Fatalf("String(app.name) = %q, want hello-app", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
1
examples/hello/config/app.yaml
Normal file
1
examples/hello/config/app.yaml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
name: hello-app
|
||||||
30
examples/hello/go.mod
Normal file
30
examples/hello/go.mod
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
module git.golem15.com/golem15/summercms/examples/hello
|
||||||
|
|
||||||
|
go 1.27.0
|
||||||
|
|
||||||
|
toolchain go1.27.0
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
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/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
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
37
examples/hello/go.sum
Normal file
37
examples/hello/go.sum
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
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/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/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=
|
||||||
|
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=
|
||||||
17
examples/hello/hello_test.go
Normal file
17
examples/hello/hello_test.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGreeterHelloPrintsAppName(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := run([]string{"greeter:hello"}, &buf); err != nil {
|
||||||
|
t.Fatalf("greeter:hello: %v", err)
|
||||||
|
}
|
||||||
|
if got := strings.TrimSpace(buf.String()); got != "hello-app" {
|
||||||
|
t.Fatalf("output = %q, want %q", got, "hello-app")
|
||||||
|
}
|
||||||
|
}
|
||||||
43
examples/hello/main.go
Normal file
43
examples/hello/main.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// Code generated by summer build. DO NOT EDIT.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.golem15.com/golem15/summercms/backpack"
|
||||||
|
"git.golem15.com/golem15/summercms/bonfire"
|
||||||
|
"git.golem15.com/golem15/summercms/compass"
|
||||||
|
"git.golem15.com/golem15/summercms/pact"
|
||||||
|
"git.golem15.com/golem15/summercms/party"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := run(os.Args[1:], os.Stdout); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(args []string, out io.Writer) error {
|
||||||
|
cfg, err := compass.Load("config")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
app := backpack.New(cfg)
|
||||||
|
plugins, err := party.Activate(app, PluginIDs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var commands []bonfire.Command
|
||||||
|
for _, plugin := range plugins {
|
||||||
|
if hasCommands, ok := plugin.(pact.HasCommands); ok {
|
||||||
|
commands = append(commands, hasCommands.Commands()...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
root := bonfire.NewRoot("hello", commands, out)
|
||||||
|
root.SetArgs(args)
|
||||||
|
return root.Execute()
|
||||||
|
}
|
||||||
14
examples/hello/plugins.gen.go
Normal file
14
examples/hello/plugins.gen.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// Code generated by summer build. DO NOT EDIT.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "git.golem15.com/golem15/summercms/examples/hello/plugins/base"
|
||||||
|
_ "git.golem15.com/golem15/summercms/examples/hello/plugins/greeter"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PluginIDs is the ordered plugin list from summer.yaml.
|
||||||
|
var PluginIDs = []string{
|
||||||
|
"golem15.hello",
|
||||||
|
"golem15.greeter",
|
||||||
|
}
|
||||||
19
examples/hello/plugins/base/go.mod
Normal file
19
examples/hello/plugins/base/go.mod
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
module git.golem15.com/golem15/summercms/examples/hello/plugins/base
|
||||||
|
|
||||||
|
go 1.27.0
|
||||||
|
|
||||||
|
toolchain go1.27.0
|
||||||
|
|
||||||
|
require git.golem15.com/golem15/summercms v0.0.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.4.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/v2 v2.3.6 // indirect
|
||||||
|
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||||
|
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
replace git.golem15.com/golem15/summercms => ../../../..
|
||||||
29
examples/hello/plugins/base/go.sum
Normal file
29
examples/hello/plugins/base/go.sum
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
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/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/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/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/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=
|
||||||
|
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=
|
||||||
19
examples/hello/plugins/base/plugin.go
Normal file
19
examples/hello/plugins/base/plugin.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package base
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.golem15.com/golem15/summercms/backpack"
|
||||||
|
"git.golem15.com/golem15/summercms/party"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Plugin is the golem15.hello plugin.
|
||||||
|
type Plugin struct{}
|
||||||
|
|
||||||
|
func (p *Plugin) ID() string { return "golem15.hello" }
|
||||||
|
func (p *Plugin) Requires() []string { return nil }
|
||||||
|
|
||||||
|
func (p *Plugin) Register(*backpack.App) error { return nil }
|
||||||
|
func (p *Plugin) Boot(*backpack.App) error { return nil }
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
party.Register(&Plugin{})
|
||||||
|
}
|
||||||
22
examples/hello/plugins/greeter/go.mod
Normal file
22
examples/hello/plugins/greeter/go.mod
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
module git.golem15.com/golem15/summercms/examples/hello/plugins/greeter
|
||||||
|
|
||||||
|
go 1.27.0
|
||||||
|
|
||||||
|
toolchain go1.27.0
|
||||||
|
|
||||||
|
require git.golem15.com/golem15/summercms v0.0.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
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/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
|
||||||
|
)
|
||||||
|
|
||||||
|
replace git.golem15.com/golem15/summercms => ../../../..
|
||||||
37
examples/hello/plugins/greeter/go.sum
Normal file
37
examples/hello/plugins/greeter/go.sum
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
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/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/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=
|
||||||
|
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=
|
||||||
43
examples/hello/plugins/greeter/plugin.go
Normal file
43
examples/hello/plugins/greeter/plugin.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package greeter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"git.golem15.com/golem15/summercms/backpack"
|
||||||
|
"git.golem15.com/golem15/summercms/bonfire"
|
||||||
|
"git.golem15.com/golem15/summercms/party"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Plugin is the golem15.greeter plugin. It requires golem15.hello.
|
||||||
|
type Plugin struct {
|
||||||
|
app *backpack.App
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) ID() string { return "golem15.greeter" }
|
||||||
|
func (p *Plugin) Requires() []string { return []string{"golem15.hello"} }
|
||||||
|
|
||||||
|
func (p *Plugin) Register(*backpack.App) error { return nil }
|
||||||
|
|
||||||
|
func (p *Plugin) Boot(app *backpack.App) error {
|
||||||
|
p.app = app
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) Commands() []bonfire.Command {
|
||||||
|
return []bonfire.Command{{
|
||||||
|
Name: "greeter:hello",
|
||||||
|
Description: "Print the configured application name",
|
||||||
|
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
||||||
|
name := ""
|
||||||
|
if p.app != nil && p.app.Config != nil {
|
||||||
|
name = p.app.Config.String("app.name")
|
||||||
|
}
|
||||||
|
out.Println(name)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
party.Register(&Plugin{})
|
||||||
|
}
|
||||||
7
examples/hello/summer.yaml
Normal file
7
examples/hello/summer.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
module: git.golem15.com/golem15/summercms/examples/hello
|
||||||
|
binary: hello
|
||||||
|
plugins:
|
||||||
|
- id: golem15.hello
|
||||||
|
module: git.golem15.com/golem15/summercms/examples/hello/plugins/base
|
||||||
|
- id: golem15.greeter
|
||||||
|
module: git.golem15.com/golem15/summercms/examples/hello/plugins/greeter
|
||||||
18
go.mod
18
go.mod
@@ -1,3 +1,21 @@
|
|||||||
module git.golem15.com/golem15/summercms
|
module git.golem15.com/golem15/summercms
|
||||||
|
|
||||||
go 1.27.0
|
go 1.27.0
|
||||||
|
|
||||||
|
toolchain go1.27.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/knadh/koanf/parsers/yaml v1.1.1
|
||||||
|
github.com/knadh/koanf/v2 v2.3.6
|
||||||
|
github.com/spf13/cobra v1.10.2
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
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/mitchellh/copystructure v1.2.0 // indirect
|
||||||
|
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.9 // indirect
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
|
)
|
||||||
|
|||||||
37
go.sum
Normal file
37
go.sum
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
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/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/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=
|
||||||
|
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=
|
||||||
10
go.work
Normal file
10
go.work
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
go 1.27.0
|
||||||
|
|
||||||
|
toolchain go1.27.0
|
||||||
|
|
||||||
|
use (
|
||||||
|
.
|
||||||
|
./examples/hello
|
||||||
|
./examples/hello/plugins/base
|
||||||
|
./examples/hello/plugins/greeter
|
||||||
|
)
|
||||||
34
pact/capabilities.go
Normal file
34
pact/capabilities.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package pact
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
|
||||||
|
"git.golem15.com/golem15/summercms/bonfire"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HasCommands is implemented by plugins that register console commands.
|
||||||
|
type HasCommands interface {
|
||||||
|
Commands() []bonfire.Command
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasConfig is implemented by plugins that ship default YAML configuration.
|
||||||
|
// ConfigFS should contain files such as config/config.yaml, merged later at
|
||||||
|
// the plugin ID path.
|
||||||
|
type HasConfig interface {
|
||||||
|
ConfigFS() fs.FS
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reserved KERN-03 capability names, declared when their first consumers exist:
|
||||||
|
//
|
||||||
|
// HasModels
|
||||||
|
// HasMigrations
|
||||||
|
// HasRoutes
|
||||||
|
// HasMiddleware
|
||||||
|
// HasJobs
|
||||||
|
// HasListeners
|
||||||
|
// HasAdminControllers
|
||||||
|
// HasNavigation
|
||||||
|
// HasPermissions
|
||||||
|
// HasSchedule
|
||||||
|
// HasMailTemplates
|
||||||
|
// HasLang
|
||||||
195
party/registry.go
Normal file
195
party/registry.go
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
package party
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"git.golem15.com/golem15/summercms/backpack"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Plugin is the required descriptor every compiled plugin implements.
|
||||||
|
type Plugin interface {
|
||||||
|
ID() string
|
||||||
|
Requires() []string
|
||||||
|
Register(app *backpack.App) error
|
||||||
|
Boot(app *backpack.App) error
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
mu sync.Mutex
|
||||||
|
plugins []Plugin
|
||||||
|
)
|
||||||
|
|
||||||
|
// Register appends a plugin to the process-wide registry. Plugins call this
|
||||||
|
// from init() after a blank import; Activate selects and orders them.
|
||||||
|
func Register(p Plugin) {
|
||||||
|
if p == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
plugins = append(plugins, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func snapshot() []Plugin {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
out := make([]Plugin, len(plugins))
|
||||||
|
copy(out, plugins)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activate resolves ids in manifest order, topologically sorts Requires(),
|
||||||
|
// then runs every Register before any Boot.
|
||||||
|
func Activate(app *backpack.App, ids []string) ([]Plugin, error) {
|
||||||
|
if app == nil {
|
||||||
|
return nil, fmt.Errorf("party: app is nil")
|
||||||
|
}
|
||||||
|
return activate(snapshot(), app, ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
func activate(registered []Plugin, app *backpack.App, ids []string) ([]Plugin, error) {
|
||||||
|
byID := make(map[string]Plugin, len(registered))
|
||||||
|
for _, p := range registered {
|
||||||
|
if p == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
id := p.ID()
|
||||||
|
if id == "" {
|
||||||
|
return nil, fmt.Errorf("party: registered plugin has empty id")
|
||||||
|
}
|
||||||
|
if _, exists := byID[id]; exists {
|
||||||
|
return nil, fmt.Errorf("party: duplicate plugin id %q", id)
|
||||||
|
}
|
||||||
|
byID[id] = p
|
||||||
|
}
|
||||||
|
|
||||||
|
seen := make(map[string]struct{}, len(ids))
|
||||||
|
selected := make([]Plugin, 0, len(ids))
|
||||||
|
index := make(map[string]int, len(ids))
|
||||||
|
for _, id := range ids {
|
||||||
|
if id == "" {
|
||||||
|
return nil, fmt.Errorf("party: empty plugin id in manifest")
|
||||||
|
}
|
||||||
|
if _, dup := seen[id]; dup {
|
||||||
|
return nil, fmt.Errorf("party: duplicate plugin id %q", id)
|
||||||
|
}
|
||||||
|
seen[id] = struct{}{}
|
||||||
|
p, ok := byID[id]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("party: plugin %q is not registered", id)
|
||||||
|
}
|
||||||
|
index[id] = len(selected)
|
||||||
|
selected = append(selected, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range selected {
|
||||||
|
for _, req := range p.Requires() {
|
||||||
|
if _, ok := seen[req]; !ok {
|
||||||
|
return nil, fmt.Errorf("party: plugin %q requires %q, which is not registered", p.ID(), req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ordered, err := topoSort(selected, index)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range ordered {
|
||||||
|
if err := p.Register(app); err != nil {
|
||||||
|
return nil, fmt.Errorf("party: register %s: %w", p.ID(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, p := range ordered {
|
||||||
|
if err := p.Boot(app); err != nil {
|
||||||
|
return nil, fmt.Errorf("party: boot %s: %w", p.ID(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ordered, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func topoSort(plugins []Plugin, index map[string]int) ([]Plugin, error) {
|
||||||
|
byID := make(map[string]Plugin, len(plugins))
|
||||||
|
indeg := make(map[string]int, len(plugins))
|
||||||
|
dependents := make(map[string][]string, len(plugins))
|
||||||
|
for _, p := range plugins {
|
||||||
|
byID[p.ID()] = p
|
||||||
|
indeg[p.ID()] = 0
|
||||||
|
}
|
||||||
|
for _, p := range plugins {
|
||||||
|
seenReq := make(map[string]struct{})
|
||||||
|
for _, req := range p.Requires() {
|
||||||
|
if _, ok := seenReq[req]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seenReq[req] = struct{}{}
|
||||||
|
if _, ok := byID[req]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
indeg[p.ID()]++
|
||||||
|
dependents[req] = append(dependents[req], p.ID())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ready := make([]string, 0, len(plugins))
|
||||||
|
for _, p := range plugins {
|
||||||
|
if indeg[p.ID()] == 0 {
|
||||||
|
ready = append(ready, p.ID())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]Plugin, 0, len(plugins))
|
||||||
|
for len(ready) > 0 {
|
||||||
|
id := ready[0]
|
||||||
|
ready = ready[1:]
|
||||||
|
out = append(out, byID[id])
|
||||||
|
next := append([]string(nil), dependents[id]...)
|
||||||
|
sortByIndex(next, index)
|
||||||
|
for _, dep := range next {
|
||||||
|
indeg[dep]--
|
||||||
|
if indeg[dep] == 0 {
|
||||||
|
ready = insertByIndex(ready, dep, index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(out) != len(plugins) {
|
||||||
|
leftover := make([]string, 0)
|
||||||
|
done := make(map[string]struct{}, len(out))
|
||||||
|
for _, p := range out {
|
||||||
|
done[p.ID()] = struct{}{}
|
||||||
|
}
|
||||||
|
for _, p := range plugins {
|
||||||
|
if _, ok := done[p.ID()]; !ok {
|
||||||
|
leftover = append(leftover, p.ID())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("party: dependency cycle involving %s", strings.Join(leftover, ", "))
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortByIndex(ids []string, index map[string]int) {
|
||||||
|
for i := 1; i < len(ids); i++ {
|
||||||
|
j := i
|
||||||
|
for j > 0 && index[ids[j]] < index[ids[j-1]] {
|
||||||
|
ids[j], ids[j-1] = ids[j-1], ids[j]
|
||||||
|
j--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func insertByIndex(ready []string, id string, index map[string]int) []string {
|
||||||
|
at := len(ready)
|
||||||
|
for i, existing := range ready {
|
||||||
|
if index[id] < index[existing] {
|
||||||
|
at = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ready = append(ready, "")
|
||||||
|
copy(ready[at+1:], ready[at:])
|
||||||
|
ready[at] = id
|
||||||
|
return ready
|
||||||
|
}
|
||||||
56
party/registry_test.go
Normal file
56
party/registry_test.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package party
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.golem15.com/golem15/summercms/backpack"
|
||||||
|
"git.golem15.com/golem15/summercms/compass"
|
||||||
|
)
|
||||||
|
|
||||||
|
type tracePlugin struct {
|
||||||
|
id string
|
||||||
|
req []string
|
||||||
|
log *[]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *tracePlugin) ID() string { return p.id }
|
||||||
|
func (p *tracePlugin) Requires() []string { return p.req }
|
||||||
|
|
||||||
|
func (p *tracePlugin) Register(*backpack.App) error {
|
||||||
|
*p.log = append(*p.log, p.id+":register")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *tracePlugin) Boot(*backpack.App) error {
|
||||||
|
*p.log = append(*p.log, p.id+":boot")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActivateRunsAllRegisterBeforeAnyBoot(t *testing.T) {
|
||||||
|
var log []string
|
||||||
|
hello := &tracePlugin{id: "golem15.hello", log: &log}
|
||||||
|
greeter := &tracePlugin{id: "golem15.greeter", req: []string{"golem15.hello"}, log: &log}
|
||||||
|
app := backpack.New(&compass.Config{})
|
||||||
|
|
||||||
|
got, err := activate([]Plugin{greeter, hello}, app, []string{"golem15.greeter", "golem15.hello"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("activate: %v", err)
|
||||||
|
}
|
||||||
|
if len(got) != 2 {
|
||||||
|
t.Fatalf("got %d plugins, want 2", len(got))
|
||||||
|
}
|
||||||
|
if got[0].ID() != "golem15.hello" || got[1].ID() != "golem15.greeter" {
|
||||||
|
t.Fatalf("order = %s, %s", got[0].ID(), got[1].ID())
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []string{
|
||||||
|
"golem15.hello:register",
|
||||||
|
"golem15.greeter:register",
|
||||||
|
"golem15.hello:boot",
|
||||||
|
"golem15.greeter:boot",
|
||||||
|
}
|
||||||
|
if strings.Join(log, ",") != strings.Join(want, ",") {
|
||||||
|
t.Fatalf("lifecycle = %v, want %v", log, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user