GUI packages, examples, and documentation for building desktop applications with Go and web technologies. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
307 lines
7.2 KiB
Go
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(),
|
|
})
|
|
}
|