- Build-time discovery (D-028, the Caddy pattern): a module self-registers from init(); internal/modules/imports_gen.go (go:generate) blank-imports every module directory, so THE DIRECTORY IS THE SOURCE OF TRUTH - The registry: deterministic registration order, config-backed enable/disable (D-038), required modules, name-prefixed migrations, Mount() into the door - The name-collision guard now also fails a module named for a reserved level-1 workflow (SPEC-009 §13 Q-1) — access · static · api · media · ws · dev — derived from routing.ReservedWorkflows, `home` excepted (D-2) - home: the reference dashboard module, and the core Modules page
150 lines
5.5 KiB
Go
150 lines
5.5 KiB
Go
// Package module is MyDay's module registry (SPEC-008): the module CONTRACT,
|
|
// the runtime registry, the build-time DISCOVERY seam, and the core Modules
|
|
// page.
|
|
//
|
|
// Discovery is at BUILD time, not runtime (D-028, the Caddy pattern): a module
|
|
// is a package under internal/modules/<name>/ that self-registers from its
|
|
// init() by calling Add; a generated aggregator (internal/modules/imports_gen.go,
|
|
// produced by `go generate`) blank-imports every module directory, so THE
|
|
// DIRECTORY IS THE SOURCE OF TRUTH. Adding a module is: add the directory →
|
|
// `go generate` → rebuild. No core edits.
|
|
//
|
|
// The registry makes the loaded set explicit and deterministic: modules keep
|
|
// their registration order (never map iteration), the loaded set is logged at
|
|
// startup, and enable/disable is config-backed (D-038) — a disabled module is
|
|
// not mounted, so its routes 404; its data is never deleted.
|
|
//
|
|
// Spec: docs/specs/spec-008-module-registry.md.
|
|
package module
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"sync"
|
|
|
|
"git.lohmar.co.uk/cclohmar/NextMyDay/internal/core/server"
|
|
"git.lohmar.co.uk/cclohmar/NextMyDay/internal/core/store"
|
|
)
|
|
|
|
// Migration is one ordered, named schema migration owned by a module. It reuses
|
|
// the core runner's type so the CORE owns the runner (SPEC-008 §4/AC-5): the
|
|
// registry prefixes each version with the module name, avoiding collisions with
|
|
// the core schema and between modules.
|
|
type Migration = store.Migration
|
|
|
|
// Handler is a dispatched module function. It is the door's handler signature:
|
|
// the module receives the resolved RequestContext — identification and routing,
|
|
// never a raw path (the rule of the door, AGENT.md §3.2).
|
|
type Handler = server.Handler
|
|
|
|
// Route is one path-mode function a module declares (SPEC-008 §4, §4.2):
|
|
// `/{GUID}/{module}/{name}/…` resolves to Function.
|
|
type Route struct {
|
|
// Name is the PARAM3 value the route answers (e.g. "pto"). Ignored when
|
|
// Default is set.
|
|
Name string
|
|
// Function is the resolved function name the door dispatches on.
|
|
Function string
|
|
// Handler runs the function.
|
|
Handler Handler
|
|
// Default marks the route served when PARAM3 is absent or trailing
|
|
// (SPEC-002 §3.4): a module declares at most one.
|
|
Default bool
|
|
}
|
|
|
|
// Action is one action-mode function a module declares (SPEC-008 §4, §4.3):
|
|
// `POST /{GUID}?do={module}&action={name}` resolves to Function.
|
|
type Action struct {
|
|
// Name is the `action` value the module answers.
|
|
Name string
|
|
// Function is the resolved function name the door dispatches on.
|
|
Function string
|
|
// Handler runs the function.
|
|
Handler Handler
|
|
}
|
|
|
|
// Module is the module contract every module implements (SPEC-008 §4). The
|
|
// default skeleton lives in internal/modules/<name>/ as app.go (this manifest) ·
|
|
// routing.go (Routes/Actions) · handlers.go (the handlers), per D-037.
|
|
type Module interface {
|
|
// Name is the PARAM2 segment — the registry key ("home", "time", …).
|
|
Name() string
|
|
|
|
// Required reports whether the module is always on. A required module
|
|
// cannot be disabled (D-038, AC-4).
|
|
Required() bool
|
|
|
|
// Migrations is the module's own schema, versioned and applied in
|
|
// registration order by the core runner (AC-5).
|
|
Migrations() []Migration
|
|
|
|
// Routes are the module's path-mode functions (§4.2).
|
|
Routes() []Route
|
|
|
|
// Actions are the module's action-mode functions (§4.3).
|
|
Actions() []Action
|
|
|
|
// Register adds the module to the runtime registry. The loader calls it
|
|
// once per discovered module; the usual implementation is `r.Add(m)`.
|
|
Register(r *Registry) error
|
|
|
|
// Start is OPTIONAL background work (integrations, sync). A module without
|
|
// background work returns nil.
|
|
Start(ctx context.Context) error
|
|
}
|
|
|
|
// Kind is an OPTIONAL capability a module may implement to declare its
|
|
// catalogue kind (SPEC-008 §4.8: records · domain · workflow · analytical ·
|
|
// integration). The Modules page lists it (§7); absent, KindOf reports
|
|
// "module".
|
|
type Kind interface {
|
|
Kind() string
|
|
}
|
|
|
|
// KindOf returns a module's kind for the Modules page, defaulting to "module"
|
|
// when the module does not implement Kind.
|
|
func KindOf(m Module) string {
|
|
if k, ok := m.(Kind); ok {
|
|
if s := strings.TrimSpace(k.Kind()); s != "" {
|
|
return s
|
|
}
|
|
}
|
|
return "module"
|
|
}
|
|
|
|
// ── build-time discovery (D-028) ─────────────────────────────────────────────
|
|
|
|
// discovered is the process-wide self-registration list. Modules append to it
|
|
// from init(); because the generated aggregator blank-imports every module
|
|
// directory IN SORTED ORDER, Go runs those init() functions in that order and
|
|
// the list is deterministic (D-028 guardrail: never map iteration).
|
|
var (
|
|
discoveryMu sync.Mutex
|
|
discovered []Module
|
|
)
|
|
|
|
// Add self-registers a module from its package init() (D-028). It is called by
|
|
// each module directory; the generated aggregator guarantees the package is
|
|
// imported, so the directory is the source of truth.
|
|
//
|
|
// A duplicate name is not detected here (init cannot fail usefully): the
|
|
// runtime Add catches it when Load builds the registry, which is loud and
|
|
// testable.
|
|
func Add(m Module) {
|
|
if m == nil {
|
|
return
|
|
}
|
|
discoveryMu.Lock()
|
|
discovered = append(discovered, m)
|
|
discoveryMu.Unlock()
|
|
}
|
|
|
|
// Discovered returns the process-wide, self-registered modules in registration
|
|
// order. It returns a copy: the caller may not mutate the registry through it.
|
|
func Discovered() []Module {
|
|
discoveryMu.Lock()
|
|
defer discoveryMu.Unlock()
|
|
out := make([]Module, len(discovered))
|
|
copy(out, discovered)
|
|
return out
|
|
}
|