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:
@@ -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
60
backpack/services.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user