refactor(10.2-01): nest framework packages under modules
- Move remaining beach packages and embedded admin assets\n- Rewrite framework, example, build, and gate paths
This commit is contained in:
74
modules/backpack/app.go
Normal file
74
modules/backpack/app.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package backpack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/compass"
|
||||
"git.golem15.com/golem15/summercms/modules/festival"
|
||||
)
|
||||
|
||||
// App is the per-instance application container. It must not import party.
|
||||
type App struct {
|
||||
Config *compass.Config
|
||||
Services *Registry
|
||||
Events *festival.Bus
|
||||
|
||||
mu sync.RWMutex
|
||||
plugins map[string]struct{}
|
||||
}
|
||||
|
||||
// New returns an App that owns cfg.
|
||||
func New(cfg *compass.Config) *App {
|
||||
return &App{
|
||||
Config: cfg,
|
||||
Services: NewRegistry(),
|
||||
Events: festival.New(),
|
||||
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]()
|
||||
}
|
||||
50
modules/backpack/app_test.go
Normal file
50
modules/backpack/app_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package backpack
|
||||
|
||||
import (
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/compass"
|
||||
)
|
||||
|
||||
func TestEventBusesAreAppScoped(t *testing.T) {
|
||||
a := New(&compass.Config{})
|
||||
b := New(&compass.Config{})
|
||||
if a.Events == nil || b.Events == nil {
|
||||
t.Fatal("App must own an event bus")
|
||||
}
|
||||
if a.Events == b.Events {
|
||||
t.Fatal("apps must not share a bus")
|
||||
}
|
||||
}
|
||||
|
||||
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/modules/party" {
|
||||
t.Fatalf("%s imports party", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
60
modules/backpack/services.go
Normal file
60
modules/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
|
||||
}
|
||||
161
modules/backpack/services_test.go
Normal file
161
modules/backpack/services_test.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package backpack
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/compass"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
)
|
||||
|
||||
type ping interface {
|
||||
Ping() string
|
||||
}
|
||||
|
||||
type pingImpl struct{ s string }
|
||||
|
||||
func (p pingImpl) Ping() string { return p.s }
|
||||
|
||||
func TestPublishLookup(t *testing.T) {
|
||||
app := New(&compass.Config{})
|
||||
if err := app.Publish[ping](pingImpl{s: "pong"}); err != nil {
|
||||
t.Fatalf("Publish: %v", err)
|
||||
}
|
||||
got, ok := app.Lookup[ping]()
|
||||
if !ok {
|
||||
t.Fatal("Lookup returned false")
|
||||
}
|
||||
if got.Ping() != "pong" {
|
||||
t.Fatalf("Ping = %q, want pong", got.Ping())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLookupAbsentInterface(t *testing.T) {
|
||||
app := New(&compass.Config{})
|
||||
got, ok := app.Lookup[ping]()
|
||||
if ok {
|
||||
t.Fatalf("Lookup = (%v, true), want false", got)
|
||||
}
|
||||
if got != nil {
|
||||
t.Fatalf("absent lookup value = %v, want nil", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishDuplicateRejected(t *testing.T) {
|
||||
app := New(&compass.Config{})
|
||||
if err := app.Publish[ping](pingImpl{s: "a"}); err != nil {
|
||||
t.Fatalf("Publish: %v", err)
|
||||
}
|
||||
if err := app.Publish[ping](pingImpl{s: "b"}); err == nil {
|
||||
t.Fatal("expected duplicate publish error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceRegistriesAreAppScoped(t *testing.T) {
|
||||
a := New(&compass.Config{})
|
||||
b := New(&compass.Config{})
|
||||
if err := a.Publish[ping](pingImpl{s: "a"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := b.Lookup[ping](); ok {
|
||||
t.Fatal("app B should not see app A's service")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasPluginUsesCompleteSet(t *testing.T) {
|
||||
app := New(&compass.Config{})
|
||||
app.SetPlugins([]string{"golem15.hello", "golem15.optional"})
|
||||
if !app.HasPlugin("golem15.optional") {
|
||||
t.Fatal("HasPlugin(golem15.optional) = false")
|
||||
}
|
||||
if app.HasPlugin("golem15.missing") {
|
||||
t.Fatal("HasPlugin(golem15.missing) = true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTwoAppsIsolateHasPluginAndServices(t *testing.T) {
|
||||
a := New(&compass.Config{})
|
||||
b := New(&compass.Config{})
|
||||
a.SetPlugins([]string{"golem15.optional"})
|
||||
b.SetPlugins([]string{"golem15.hello"})
|
||||
if err := a.Publish[pact.OptionalMessage](msgService{s: "from-a"}); err != nil {
|
||||
t.Fatalf("Publish A: %v", err)
|
||||
}
|
||||
|
||||
if !a.HasPlugin("golem15.optional") {
|
||||
t.Fatal("app A should see optional")
|
||||
}
|
||||
if a.HasPlugin("golem15.hello") {
|
||||
t.Fatal("app A should not see hello")
|
||||
}
|
||||
if !b.HasPlugin("golem15.hello") {
|
||||
t.Fatal("app B should see hello")
|
||||
}
|
||||
if b.HasPlugin("golem15.optional") {
|
||||
t.Fatal("app B should not see optional")
|
||||
}
|
||||
|
||||
got, ok := a.Lookup[pact.OptionalMessage]()
|
||||
if !ok || got.Message() != "from-a" {
|
||||
t.Fatalf("app A lookup = (%v, %v)", got, ok)
|
||||
}
|
||||
if _, ok := b.Lookup[pact.OptionalMessage](); ok {
|
||||
t.Fatal("app B must not see app A's OptionalMessage")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishNilRejectedAndLookupStaysAbsent(t *testing.T) {
|
||||
app := New(&compass.Config{})
|
||||
var nilPing ping
|
||||
if err := app.Publish[ping](nilPing); err == nil {
|
||||
t.Fatal("expected nil publish error")
|
||||
}
|
||||
if _, ok := app.Lookup[ping](); ok {
|
||||
t.Fatal("nil publish must not store a value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDuplicatePublishKeepsFirstValue(t *testing.T) {
|
||||
app := New(&compass.Config{})
|
||||
if err := app.Publish[ping](pingImpl{s: "first"}); err != nil {
|
||||
t.Fatalf("Publish: %v", err)
|
||||
}
|
||||
if err := app.Publish[ping](pingImpl{s: "second"}); err == nil {
|
||||
t.Fatal("expected duplicate error")
|
||||
}
|
||||
got, ok := app.Lookup[ping]()
|
||||
if !ok || got.Ping() != "first" {
|
||||
t.Fatalf("Lookup = (%v, %v), want first", got, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcurrentLookupAndHasPlugin(t *testing.T) {
|
||||
app := New(&compass.Config{})
|
||||
app.SetPlugins([]string{"golem15.hello"})
|
||||
if err := app.Publish[ping](pingImpl{s: "pong"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 16; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for j := 0; j < 80; j++ {
|
||||
got, ok := app.Lookup[ping]()
|
||||
if !ok || got.Ping() != "pong" {
|
||||
t.Errorf("Lookup = (%v, %v)", got, ok)
|
||||
return
|
||||
}
|
||||
if !app.HasPlugin("golem15.hello") || app.HasPlugin("golem15.missing") {
|
||||
t.Error("HasPlugin concurrent mismatch")
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
type msgService struct{ s string }
|
||||
|
||||
func (m msgService) Message() string { return m.s }
|
||||
Reference in New Issue
Block a user