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
164 lines
4.8 KiB
Go
164 lines
4.8 KiB
Go
// 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
|
|
}
|