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

@@ -1,13 +1,71 @@
package backpack
import "git.golem15.com/golem15/summercms/compass"
import (
"fmt"
"sync"
"git.golem15.com/golem15/summercms/compass"
)
// App is the per-instance application container. It must not import party.
type App struct {
Config *compass.Config
Config *compass.Config
Services *Registry
mu sync.RWMutex
plugins map[string]struct{}
}
// New returns an App that owns cfg.
func New(cfg *compass.Config) *App {
return &App{Config: cfg}
return &App{
Config: cfg,
Services: NewRegistry(),
plugins: make(map[string]struct{}),
}
}
// SetPlugins records the complete activated plugin set. HasPlugin uses this
// list, including plugins that have not yet Booted.
func (a *App) SetPlugins(ids []string) {
if a == nil {
return
}
next := make(map[string]struct{}, len(ids))
for _, id := range ids {
if id != "" {
next[id] = struct{}{}
}
}
a.mu.Lock()
a.plugins = next
a.mu.Unlock()
}
// HasPlugin reports whether id is in the complete registered set.
func (a *App) HasPlugin(id string) bool {
if a == nil {
return false
}
a.mu.RLock()
defer a.mu.RUnlock()
_, ok := a.plugins[id]
return ok
}
// Publish stores an app-scoped service under T.
func (a *App) Publish[T any](value T) error {
if a == nil || a.Services == nil {
return fmt.Errorf("backpack: app has no service registry")
}
return a.Services.Publish(value)
}
// Lookup returns the app-scoped service published under T.
func (a *App) Lookup[T any]() (T, bool) {
var zero T
if a == nil || a.Services == nil {
return zero, false
}
return a.Services.Lookup[T]()
}

60
backpack/services.go Normal file
View File

@@ -0,0 +1,60 @@
package backpack
import (
"fmt"
"reflect"
"sync"
)
// Registry is an app-scoped typed service catalog. It has no process-global state.
type Registry struct {
mu sync.RWMutex
m map[reflect.Type]any
}
// NewRegistry returns an empty service registry.
func NewRegistry() *Registry {
return &Registry{m: make(map[reflect.Type]any)}
}
// Publish stores value under the concrete type argument T. Publishing the
// same T twice is an error.
func (r *Registry) Publish[T any](value T) error {
if r == nil {
return fmt.Errorf("backpack: registry is nil")
}
if any(value) == nil {
return fmt.Errorf("backpack: publish nil service")
}
key := reflect.TypeFor[T]()
r.mu.Lock()
defer r.mu.Unlock()
if r.m == nil {
r.m = make(map[reflect.Type]any)
}
if _, exists := r.m[key]; exists {
return fmt.Errorf("backpack: duplicate provider for %s", key)
}
r.m[key] = value
return nil
}
// Lookup returns the service published under T, or (zero, false).
func (r *Registry) Lookup[T any]() (T, bool) {
var zero T
if r == nil {
return zero, false
}
key := reflect.TypeFor[T]()
r.mu.RLock()
defer r.mu.RUnlock()
v, ok := r.m[key]
if !ok {
return zero, false
}
got, ok := v.(T)
if !ok {
return zero, false
}
return got, true
}