gui/pkg/module/registry.go
Snider 032c426ac3 feat: initial Wails v3 desktop framework
GUI packages, examples, and documentation for building
desktop applications with Go and web technologies.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 08:44:23 +00:00

307 lines
7.2 KiB
Go

package module
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"github.com/gin-gonic/gin"
)
// Registry manages module registration and provides unified API routing + UI assembly.
type Registry struct {
mu sync.RWMutex
modules map[string]*Module // key: code
activeContext Context
engine *gin.Engine
api *gin.RouterGroup
assetHandler http.Handler
appsDir string // Directory to scan for dynamic modules
}
// NewRegistry creates a new module registry.
func NewRegistry() *Registry {
engine := gin.New()
engine.Use(gin.Recovery())
r := &Registry{
modules: make(map[string]*Module),
activeContext: ContextDefault,
engine: engine,
api: engine.Group("/api"),
appsDir: "apps",
}
// Root API endpoint lists modules
r.api.GET("", r.handleModuleList)
r.api.GET("/", r.handleModuleList)
return r
}
// SetAppsDir sets the directory to scan for dynamic modules.
func (r *Registry) SetAppsDir(dir string) {
r.appsDir = dir
}
// SetAssetHandler sets the fallback handler for non-API routes.
func (r *Registry) SetAssetHandler(h http.Handler) {
r.assetHandler = h
r.engine.NoRoute(func(c *gin.Context) {
if r.assetHandler != nil {
r.assetHandler.ServeHTTP(c.Writer, c.Request)
} else {
c.Status(http.StatusNotFound)
}
})
}
// Engine returns the underlying Gin engine.
func (r *Registry) Engine() *gin.Engine {
return r.engine
}
// ServeHTTP implements http.Handler.
func (r *Registry) ServeHTTP(w http.ResponseWriter, req *http.Request) {
r.engine.ServeHTTP(w, req)
}
// Register registers a module from its config.
func (r *Registry) Register(cfg Config) error {
r.mu.Lock()
defer r.mu.Unlock()
mod := &Module{Config: cfg}
r.modules[cfg.Code] = mod
return nil
}
// RegisterWithHandler registers a module with an HTTP handler for API routes.
func (r *Registry) RegisterWithHandler(cfg Config, handler http.Handler) error {
r.mu.Lock()
defer r.mu.Unlock()
mod := &Module{Config: cfg, Handler: handler}
r.modules[cfg.Code] = mod
// Register API routes
basePath := "/" + cfg.Namespace + "/" + cfg.Code
r.api.Any(basePath, r.wrapHandler(handler))
r.api.Any(basePath+"/*path", r.wrapHandler(handler))
return nil
}
// RegisterGinModule registers a module that provides Gin routes.
func (r *Registry) RegisterGinModule(cfg Config, gm GinModule) error {
r.mu.Lock()
defer r.mu.Unlock()
mod := &Module{Config: cfg}
r.modules[cfg.Code] = mod
// Let the module register its routes
group := r.api.Group("/" + cfg.Namespace + "/" + cfg.Code)
gm.RegisterRoutes(group)
return nil
}
// RegisterFromJSON registers a module from JSON config.
func (r *Registry) RegisterFromJSON(data []byte) error {
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return fmt.Errorf("invalid module config: %w", err)
}
return r.Register(cfg)
}
// RegisterFromFile registers a module from a .itw3.json file.
func (r *Registry) RegisterFromFile(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("reading module file: %w", err)
}
return r.RegisterFromJSON(data)
}
// LoadApps scans the apps directory and loads all .itw3.json configs.
func (r *Registry) LoadApps(ctx context.Context) error {
if r.appsDir == "" {
return nil
}
// Check if apps directory exists
if _, err := os.Stat(r.appsDir); os.IsNotExist(err) {
return nil // No apps directory, that's fine
}
return filepath.Walk(r.appsDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
if strings.HasSuffix(path, ".itw3.json") {
if err := r.RegisterFromFile(path); err != nil {
// Log but don't fail on individual module errors
fmt.Printf("Warning: failed to load module %s: %v\n", path, err)
}
}
return nil
})
}
// Unregister removes a module.
func (r *Registry) Unregister(code string) {
r.mu.Lock()
defer r.mu.Unlock()
delete(r.modules, code)
}
// Get returns a module by code.
func (r *Registry) Get(code string) (*Module, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
m, ok := r.modules[code]
return m, ok
}
// SetContext changes the active UI context.
func (r *Registry) SetContext(ctx Context) {
r.mu.Lock()
defer r.mu.Unlock()
r.activeContext = ctx
}
// GetContext returns the current context.
func (r *Registry) GetContext() Context {
r.mu.RLock()
defer r.mu.RUnlock()
return r.activeContext
}
// GetModules returns all registered module configs.
func (r *Registry) GetModules() []Config {
r.mu.RLock()
defer r.mu.RUnlock()
result := make([]Config, 0, len(r.modules))
for _, m := range r.modules {
result = append(result, m.Config)
}
return result
}
// GetMenus returns aggregated menu items filtered by the active context.
func (r *Registry) GetMenus() []MenuItem {
r.mu.RLock()
defer r.mu.RUnlock()
var result []MenuItem
for _, mod := range r.modules {
for _, menu := range mod.Config.Menu {
if r.matchesContext(menu.Contexts) {
filtered := r.filterMenuChildren(menu)
result = append(result, filtered)
}
}
}
// Sort by order
sort.Slice(result, func(i, j int) bool {
return result[i].Order < result[j].Order
})
return result
}
// GetRoutes returns aggregated routes filtered by the active context.
func (r *Registry) GetRoutes() []Route {
r.mu.RLock()
defer r.mu.RUnlock()
var result []Route
for _, mod := range r.modules {
for _, route := range mod.Config.Routes {
if r.matchesContext(route.Contexts) {
result = append(result, route)
}
}
}
return result
}
// GetUIConfig returns complete UI configuration for the current context.
func (r *Registry) GetUIConfig() UIConfig {
return UIConfig{
Context: r.GetContext(),
Menus: r.GetMenus(),
Routes: r.GetRoutes(),
Modules: r.GetModules(),
}
}
// UIConfig is the complete UI configuration for frontends.
type UIConfig struct {
Context Context `json:"context"`
Menus []MenuItem `json:"menus"`
Routes []Route `json:"routes"`
Modules []Config `json:"modules"`
}
// matchesContext checks if item should show in current context.
func (r *Registry) matchesContext(contexts []Context) bool {
if len(contexts) == 0 {
return true // No restriction = show everywhere
}
for _, ctx := range contexts {
if ctx == r.activeContext || ctx == ContextDefault {
return true
}
}
return false
}
// filterMenuChildren recursively filters menu children by context.
func (r *Registry) filterMenuChildren(menu MenuItem) MenuItem {
if len(menu.Children) == 0 {
return menu
}
filtered := menu
filtered.Children = nil
for _, child := range menu.Children {
if r.matchesContext(child.Contexts) {
filtered.Children = append(filtered.Children, r.filterMenuChildren(child))
}
}
return filtered
}
// wrapHandler wraps an http.Handler for Gin.
func (r *Registry) wrapHandler(h http.Handler) gin.HandlerFunc {
return func(c *gin.Context) {
path := c.Param("path")
if path == "" {
path = "/"
}
c.Request.URL.Path = path
h.ServeHTTP(c.Writer, c.Request)
}
}
// handleModuleList handles GET /api - returns list of modules.
func (r *Registry) handleModuleList(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"modules": r.GetModules(),
"context": r.GetContext(),
})
}