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:
Jakub Zych
2026-09-28 02:21:02 +02:00
parent ac1f6d14f4
commit 5e50b166ef
277 changed files with 303 additions and 303 deletions

74
modules/backpack/app.go Normal file
View 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]()
}

View 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)
}
}
}
}

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
}

View 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 }