feat(modules): module registry with metadata, dependencies, and defaults
In-memory registry for 10 modules (3 required, 7 optional). SetEnabled validates dependency chains: cannot enable a module whose dependencies are disabled, cannot disable a module that others depend on. 9 unit tests covering default state, toggling, dependency validation, and error cases. Ref #96
This commit is contained in:
163
internal/modules/modules.go
Normal file
163
internal/modules/modules.go
Normal file
@@ -0,0 +1,163 @@
|
||||
// Package modules provides the module registry for Silo.
|
||||
// Each module groups API endpoints, UI views, and configuration.
|
||||
// Modules can be required (always on) or optional (admin-toggleable).
|
||||
package modules
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Module IDs.
|
||||
const (
|
||||
Core = "core"
|
||||
Schemas = "schemas"
|
||||
Storage = "storage"
|
||||
Auth = "auth"
|
||||
Projects = "projects"
|
||||
Audit = "audit"
|
||||
Odoo = "odoo"
|
||||
FreeCAD = "freecad"
|
||||
Jobs = "jobs"
|
||||
DAG = "dag"
|
||||
)
|
||||
|
||||
// ModuleInfo describes a module's metadata.
|
||||
type ModuleInfo struct {
|
||||
ID string
|
||||
Name string
|
||||
Description string
|
||||
Required bool // cannot be disabled
|
||||
DefaultEnabled bool // initial state for optional modules
|
||||
DependsOn []string // module IDs this module requires
|
||||
Version string
|
||||
}
|
||||
|
||||
// registry entries with their runtime enabled state.
|
||||
type moduleState struct {
|
||||
ModuleInfo
|
||||
enabled bool
|
||||
}
|
||||
|
||||
// Registry holds all module definitions and their enabled state.
|
||||
type Registry struct {
|
||||
mu sync.RWMutex
|
||||
modules map[string]*moduleState
|
||||
}
|
||||
|
||||
// builtinModules defines the complete set of Silo modules.
|
||||
var builtinModules = []ModuleInfo{
|
||||
{ID: Core, Name: "Core PDM", Description: "Items, revisions, files, BOM, search, import/export", Required: true, Version: "0.2"},
|
||||
{ID: Schemas, Name: "Schemas", Description: "Part numbering schema parsing and segment management", Required: true},
|
||||
{ID: Storage, Name: "Storage", Description: "MinIO/S3 file storage, presigned uploads", Required: true},
|
||||
{ID: Auth, Name: "Authentication", Description: "Local, LDAP, OIDC authentication and RBAC", DefaultEnabled: true},
|
||||
{ID: Projects, Name: "Projects", Description: "Project management and item tagging", DefaultEnabled: true},
|
||||
{ID: Audit, Name: "Audit", Description: "Audit logging, completeness scoring", DefaultEnabled: true},
|
||||
{ID: Odoo, Name: "Odoo ERP", Description: "Odoo integration (config, sync-log, push/pull)", DependsOn: []string{Auth}},
|
||||
{ID: FreeCAD, Name: "Create Integration", Description: "URI scheme, executable path, client settings", DefaultEnabled: true},
|
||||
{ID: Jobs, Name: "Job Queue", Description: "Async compute jobs, runner management", DependsOn: []string{Auth}},
|
||||
{ID: DAG, Name: "Dependency DAG", Description: "Feature DAG sync, validation states, interference detection", DependsOn: []string{Jobs}},
|
||||
}
|
||||
|
||||
// NewRegistry creates a registry with all builtin modules set to their default state.
|
||||
func NewRegistry() *Registry {
|
||||
r := &Registry{modules: make(map[string]*moduleState, len(builtinModules))}
|
||||
for _, m := range builtinModules {
|
||||
enabled := m.Required || m.DefaultEnabled
|
||||
r.modules[m.ID] = &moduleState{ModuleInfo: m, enabled: enabled}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// IsEnabled returns whether a module is currently enabled.
|
||||
func (r *Registry) IsEnabled(id string) bool {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
if m, ok := r.modules[id]; ok {
|
||||
return m.enabled
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// SetEnabled changes a module's enabled state with dependency validation.
|
||||
func (r *Registry) SetEnabled(id string, enabled bool) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
m, ok := r.modules[id]
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown module %q", id)
|
||||
}
|
||||
|
||||
if m.Required {
|
||||
return fmt.Errorf("module %q is required and cannot be disabled", id)
|
||||
}
|
||||
|
||||
if enabled {
|
||||
// Check that all dependencies are enabled.
|
||||
for _, dep := range m.DependsOn {
|
||||
if dm, ok := r.modules[dep]; ok && !dm.enabled {
|
||||
return fmt.Errorf("cannot enable %q: dependency %q is disabled", id, dep)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Check that no enabled module depends on this one.
|
||||
for _, other := range r.modules {
|
||||
if !other.enabled || other.ID == id {
|
||||
continue
|
||||
}
|
||||
for _, dep := range other.DependsOn {
|
||||
if dep == id {
|
||||
return fmt.Errorf("cannot disable %q: module %q depends on it", id, other.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m.enabled = enabled
|
||||
return nil
|
||||
}
|
||||
|
||||
// All returns info for every module, sorted by ID.
|
||||
func (r *Registry) All() []ModuleInfo {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
out := make([]ModuleInfo, 0, len(r.modules))
|
||||
for _, m := range r.modules {
|
||||
out = append(out, m.ModuleInfo)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID })
|
||||
return out
|
||||
}
|
||||
|
||||
// Get returns info for a single module, or nil if not found.
|
||||
func (r *Registry) Get(id string) *ModuleInfo {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
if m, ok := r.modules[id]; ok {
|
||||
info := m.ModuleInfo
|
||||
return &info
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateDependencies checks that every enabled module's dependencies
|
||||
// are also enabled. Returns the first violation found.
|
||||
func (r *Registry) ValidateDependencies() error {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
for _, m := range r.modules {
|
||||
if !m.enabled {
|
||||
continue
|
||||
}
|
||||
for _, dep := range m.DependsOn {
|
||||
if dm, ok := r.modules[dep]; ok && !dm.enabled {
|
||||
return fmt.Errorf("module %q is enabled but its dependency %q is disabled", m.ID, dep)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user