From c2227fbb333c7f17b8a3d596c5354bec83619eac Mon Sep 17 00:00:00 2001 From: Snider Date: Wed, 18 Mar 2026 10:53:13 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20complete=20DTO=20pattern=20=E2=80=94=20?= =?UTF-8?q?struct=20literals,=20no=20constructors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - All New* constructors removed (NewApp, NewIO, NewCoreCli, NewBus, NewService, NewCoreI18n, NewConfig) - New() uses pure struct literals: &App{}, &Fs{}, &Config{ConfigOpts:}, &Cli{opts:}, &Service{}, &Ipc{}, &I18n{} - Ipc methods moved to func (c *Core) — Ipc is now a DTO - LockApply only called from WithServiceLock, not on every New() - Service map lazy-inits on first write - CliOpts DTO with Version/Name/Description Co-Authored-By: Virgil --- pkg/core/app.go | 20 ---------- pkg/core/cli.go | 49 ++++++++++--------------- pkg/core/config.go | 68 ++++++++++++++++------------------ pkg/core/contract.go | 24 ++++++------ pkg/core/core.go | 25 ++++++------- pkg/core/fs.go | 16 -------- pkg/core/i18n.go | 4 -- pkg/core/ipc.go | 87 ++++++++++---------------------------------- pkg/core/service.go | 9 ++--- pkg/core/task.go | 25 ++++++++++++- 10 files changed, 122 insertions(+), 205 deletions(-) diff --git a/pkg/core/app.go b/pkg/core/app.go index 05f3236..18e976d 100644 --- a/pkg/core/app.go +++ b/pkg/core/app.go @@ -6,7 +6,6 @@ package core import ( - "os" "os/exec" "path/filepath" ) @@ -33,25 +32,6 @@ type App struct { Runtime any } -// NewApp creates a App with the given identity. -// Filename and Path are auto-detected from the running binary. -func NewApp(name, description, version string) *App { - app := &App{ - Name: name, - Version: version, - Description: description, - } - - // Auto-detect executable identity - if exe, err := os.Executable(); err == nil { - if abs, err := filepath.Abs(exe); err == nil { - app.Path = abs - app.Filename = filepath.Base(abs) - } - } - - return app -} // Find locates a program on PATH and returns a App for it. // Returns nil if not found. diff --git a/pkg/core/cli.go b/pkg/core/cli.go index b117254..03fb869 100644 --- a/pkg/core/cli.go +++ b/pkg/core/cli.go @@ -13,9 +13,16 @@ import ( // CliAction represents a function called when a command is invoked. type CliAction func() error +// CliOpts configures a Cli. +type CliOpts struct { + Version string + Name string + Description string +} + // Cli is the CLI command framework. type Cli struct { - app *App + opts *CliOpts rootCommand *Command defaultCommand *Command preRunCommand func(*Cli) error @@ -27,14 +34,14 @@ type Cli struct { // defaultBannerFunction prints a banner for the application. func defaultBannerFunction(c *Cli) string { version := "" - if c.app != nil && c.app.Version != "" { - version = " " + c.app.Version + if c.opts != nil && c.opts.Version != "" { + version = " " + c.opts.Version } name := "" description := "" - if c.app != nil { - name = c.app.Name - description = c.app.Description + if c.opts != nil { + name = c.opts.Name + description = c.opts.Description } if description != "" { return fmt.Sprintf("%s%s - %s", name, version, description) @@ -42,24 +49,6 @@ func defaultBannerFunction(c *Cli) string { return fmt.Sprintf("%s%s", name, version) } -// NewCoreCli creates a new CLI bound to the given App identity. -func NewCoreCli(app *App) *Cli { - name := "" - description := "" - if app != nil { - name = app.Name - description = app.Description - } - - result := &Cli{ - app: app, - bannerFunction: defaultBannerFunction, - } - result.rootCommand = NewCommand(name, description) - result.rootCommand.setApp(result) - result.rootCommand.setParentCommandPath("") - return result -} // Command returns the root command. func (c *Cli) Command() *Command { @@ -68,24 +57,24 @@ func (c *Cli) Command() *Command { // Version returns the application version string. func (c *Cli) Version() string { - if c.app != nil { - return c.app.Version + if c.opts != nil { + return c.opts.Version } return "" } // Name returns the application name. func (c *Cli) Name() string { - if c.app != nil { - return c.app.Name + if c.opts != nil { + return c.opts.Name } return c.rootCommand.name } // ShortDescription returns the application short description. func (c *Cli) ShortDescription() string { - if c.app != nil { - return c.app.Description + if c.opts != nil { + return c.opts.Description } return c.rootCommand.shortdescription } diff --git a/pkg/core/config.go b/pkg/core/config.go index b3ddca0..f2b64a0 100644 --- a/pkg/core/config.go +++ b/pkg/core/config.go @@ -1,7 +1,6 @@ // SPDX-License-Identifier: EUPL-1.2 // Settings, feature flags, and typed configuration for the Core framework. -// Named after /etc — the configuration directory. package core @@ -9,62 +8,62 @@ import ( "sync" ) -// Var is a variable that can be set, unset, and queried for its state. -// Zero value is unset. +// ConfigVar is a variable that can be set, unset, and queried for its state. type ConfigVar[T any] struct { val T set bool } -// Get returns the value, or the zero value if unset. -func (v *ConfigVar[T]) Get() T { return v.val } - -// Set sets the value and marks it as set. -func (v *ConfigVar[T]) Set(val T) { v.val = val; v.set = true } - -// IsSet returns true when a value has been set. +func (v *ConfigVar[T]) Get() T { return v.val } +func (v *ConfigVar[T]) Set(val T) { v.val = val; v.set = true } func (v *ConfigVar[T]) IsSet() bool { return v.set } - -// Unset resets to zero value and marks as unset. func (v *ConfigVar[T]) Unset() { v.set = false var zero T v.val = zero } -// NewVar creates a Var with the given value (marked as set). func NewConfigVar[T any](val T) ConfigVar[T] { return ConfigVar[T]{val: val, set: true} } -// Config holds configuration settings and feature flags. -type Config struct { - mu sync.RWMutex - settings map[string]any - features map[string]bool +// ConfigOpts holds configuration data. +type ConfigOpts struct { + Settings map[string]any + Features map[string]bool } -// NewConfig creates a new configuration store. -func NewConfig() *Config { - return &Config{ - settings: make(map[string]any), - features: make(map[string]bool), +func (o *ConfigOpts) init() { + if o.Settings == nil { + o.Settings = make(map[string]any) } + if o.Features == nil { + o.Features = make(map[string]bool) + } +} + +// Config holds configuration settings and feature flags. +type Config struct { + *ConfigOpts + mu sync.RWMutex } // Set stores a configuration value by key. func (e *Config) Set(key string, val any) { e.mu.Lock() - e.settings[key] = val + e.ConfigOpts.init() + e.Settings[key] = val e.mu.Unlock() } // Get retrieves a configuration value by key. -// Returns (value, true) if found, (zero, false) if not. func (e *Config) Get(key string) (any, bool) { e.mu.RLock() - val, ok := e.settings[key] - e.mu.RUnlock() + defer e.mu.RUnlock() + if e.ConfigOpts == nil || e.Settings == nil { + return nil, false + } + val, ok := e.Settings[key] return val, ok } @@ -73,7 +72,6 @@ func (e *Config) Int(key string) int { return ConfigGet[int](e, key) } func (e *Config) Bool(key string) bool { return ConfigGet[bool](e, key) } // ConfigGet retrieves a typed configuration value. -// Returns zero value if key is missing or type doesn't match. func ConfigGet[T any](e *Config, key string) T { val, ok := e.Get(key) if !ok { @@ -86,34 +84,32 @@ func ConfigGet[T any](e *Config, key string) T { // --- Feature Flags --- -// Enable enables a feature flag. func (e *Config) Enable(feature string) { e.mu.Lock() - e.features[feature] = true + e.ConfigOpts.init() + e.Features[feature] = true e.mu.Unlock() } -// Disable disables a feature flag. func (e *Config) Disable(feature string) { e.mu.Lock() - e.features[feature] = false + e.ConfigOpts.init() + e.Features[feature] = false e.mu.Unlock() } -// Enabled returns true if the feature is enabled. func (e *Config) Enabled(feature string) bool { e.mu.RLock() - v := e.features[feature] + v := e.Features[feature] e.mu.RUnlock() return v } -// Features returns all enabled feature names. func (e *Config) EnabledFeatures() []string { e.mu.RLock() defer e.mu.RUnlock() var result []string - for k, v := range e.features { + for k, v := range e.Features { if v { result = append(result, k) } diff --git a/pkg/core/contract.go b/pkg/core/contract.go index 522f5be..8e74b0c 100644 --- a/pkg/core/contract.go +++ b/pkg/core/contract.go @@ -9,6 +9,7 @@ import ( "embed" "fmt" "io/fs" + "path/filepath" "reflect" "strings" ) @@ -88,20 +89,18 @@ type ActionTaskCompleted struct { // New creates a Core instance with the provided options. func New(opts ...Option) (*Core, error) { - defaultFS, _ := NewIO("/") - app := NewApp("", "", "") c := &Core{ - app: app, - fs: defaultFS, - cfg: &Config{settings: make(map[string]any), features: make(map[string]bool)}, + app: &App{}, + fs: &Fs{root: "/"}, + cfg: &Config{ConfigOpts: &ConfigOpts{}}, err: &ErrPan{}, log: &ErrLog{&ErrOpts{Log: defaultLog}}, - cli: NewCoreCli(app), - srv: &Service{Services: make(map[string]any)}, + cli: &Cli{opts: &CliOpts{}}, + srv: &Service{}, lock: &Lock{}, + ipc: &Ipc{}, i18n: &I18n{}, } - c.ipc = &Ipc{core: c} for _, o := range opts { if err := o(c); err != nil { @@ -109,7 +108,6 @@ func New(opts ...Option) (*Core, error) { } } - c.LockApply() return c, nil } @@ -193,11 +191,14 @@ func WithAssets(efs embed.FS) Option { // WithIO sandboxes filesystem I/O to a root path. func WithIO(root string) Option { return func(c *Core) error { - io, err := NewIO(root) + abs, err := filepath.Abs(root) if err != nil { return E("core.WithIO", "failed to create IO at "+root, err) } - c.fs = io + if resolved, err := filepath.EvalSymlinks(abs); err == nil { + abs = resolved + } + c.fs = &Fs{root: abs} return nil } } @@ -218,6 +219,7 @@ func WithMount(fsys fs.FS, basedir string) Option { func WithServiceLock() Option { return func(c *Core) error { c.LockEnable() + c.LockApply() return nil } } diff --git a/pkg/core/core.go b/pkg/core/core.go index a7861fc..a54b6f6 100644 --- a/pkg/core/core.go +++ b/pkg/core/core.go @@ -15,9 +15,9 @@ import ( // Core is the central application object that manages services, assets, and communication. type Core struct { app *App // c.App() — Application identity + optional GUI runtime - emb *Embed // c.Embed() — Mounted embedded assets (read-only) + emb *Embed // c.Embed() — Mounted embedded assets (read-only) fs *Fs // c.Fs() — Local filesystem I/O (sandboxable) - cfg *Config // c.Config() — Configuration, settings, feature flags + cfg *Config // c.Config() — Configuration, settings, feature flags err *ErrPan // c.Error() — Panic recovery and crash reporting log *ErrLog // c.Log() — Structured logging + error wrapping cli *Cli // c.Cli() — CLI command framework @@ -34,9 +34,9 @@ type Core struct { // --- Accessors --- func (c *Core) App() *App { return c.app } -func (c *Core) Embed() *Embed { return c.emb } +func (c *Core) Embed() *Embed { return c.emb } func (c *Core) Fs() *Fs { return c.fs } -func (c *Core) Config() *Config { return c.cfg } +func (c *Core) Config() *Config { return c.cfg } func (c *Core) Error() *ErrPan { return c.err } func (c *Core) Log() *ErrLog { return c.log } func (c *Core) Cli() *Cli { return c.cli } @@ -44,16 +44,14 @@ func (c *Core) IPC() *Ipc { return c.ipc } func (c *Core) I18n() *I18n { return c.i18n } func (c *Core) Core() *Core { return c } -// --- IPC --- +// --- IPC (uppercase aliases) --- -func (c *Core) ACTION(msg Message) error { return c.ipc.Action(msg) } -func (c *Core) RegisterAction(handler func(*Core, Message) error) { c.ipc.RegisterAction(handler) } -func (c *Core) RegisterActions(handlers ...func(*Core, Message) error) { c.ipc.RegisterActions(handlers...) } -func (c *Core) QUERY(q Query) (any, bool, error) { return c.ipc.Query(q) } -func (c *Core) QUERYALL(q Query) ([]any, error) { return c.ipc.QueryAll(q) } -func (c *Core) PERFORM(t Task) (any, bool, error) { return c.ipc.Perform(t) } -func (c *Core) RegisterQuery(handler QueryHandler) { c.ipc.RegisterQuery(handler) } -func (c *Core) RegisterTask(handler TaskHandler) { c.ipc.RegisterTask(handler) } +func (c *Core) ACTION(msg Message) error { return c.Action(msg) } +func (c *Core) RegisterAction(handler func(*Core, Message) error) { c.RegisterAction(handler) } +func (c *Core) RegisterActions(handlers ...func(*Core, Message) error) { c.RegisterActions(handlers...) } +func (c *Core) QUERY(q Query) (any, bool, error) { return c.Query(q) } +func (c *Core) QUERYALL(q Query) ([]any, error) { return c.QueryAll(q) } +func (c *Core) PERFORM(t Task) (any, bool, error) { return c.Perform(t) } // --- Error+Log --- @@ -73,4 +71,3 @@ func (c *Core) Must(err error, op, msg string) { } // --- Global Instance --- - diff --git a/pkg/core/fs.go b/pkg/core/fs.go index fcf4e94..d977046 100644 --- a/pkg/core/fs.go +++ b/pkg/core/fs.go @@ -17,22 +17,6 @@ type Fs struct { root string } -// NewIO creates a Fs rooted at the given directory. -// Pass "/" for full filesystem access, or a specific path to sandbox. -func NewIO(root string) (*Fs, error) { - abs, err := filepath.Abs(root) - if err != nil { - return nil, err - } - // Resolve symlinks so sandbox checks compare like-for-like. - // On macOS, /var is a symlink to /private/var — without this, - // EvalSymlinks on child paths resolves to /private/var/... while - // root stays /var/..., causing false sandbox escape detections. - if resolved, err := filepath.EvalSymlinks(abs); err == nil { - abs = resolved - } - return &Fs{root: abs}, nil -} // path sanitises and returns the full path. // Absolute paths are sandboxed under root (unless root is "/"). diff --git a/pkg/core/i18n.go b/pkg/core/i18n.go index 73acf55..e8ff836 100644 --- a/pkg/core/i18n.go +++ b/pkg/core/i18n.go @@ -47,10 +47,6 @@ type I18n struct { translator Translator // registered implementation (nil until set) } -// NewCoreI18n creates a new i18n manager. -func NewCoreI18n() *I18n { - return &I18n{} -} // AddLocales adds locale mounts (called during service registration). func (i *I18n) AddLocales(mounts ...*Embed) { diff --git a/pkg/core/ipc.go b/pkg/core/ipc.go index 223d3b6..aa66d0e 100644 --- a/pkg/core/ipc.go +++ b/pkg/core/ipc.go @@ -12,10 +12,8 @@ import ( "sync" ) -// Ipc owns action, query, and task dispatch between services. +// Ipc holds IPC dispatch data. type Ipc struct { - core *Core - ipcMu sync.RWMutex ipcHandlers []func(*Core, Message) error @@ -26,48 +24,27 @@ type Ipc struct { taskHandlers []TaskHandler } -// NewBus creates an empty message bus bound to the given Core. -func NewBus(c *Core) *Ipc { - return &Ipc{core: c} -} - -// Action dispatches a message to all registered IPC handlers. -func (b *Ipc) Action(msg Message) error { - b.ipcMu.RLock() - handlers := slices.Clone(b.ipcHandlers) - b.ipcMu.RUnlock() +func (c *Core) Action(msg Message) error { + c.ipc.ipcMu.RLock() + handlers := slices.Clone(c.ipc.ipcHandlers) + c.ipc.ipcMu.RUnlock() var agg error for _, h := range handlers { - if err := h(b.core, msg); err != nil { + if err := h(c, msg); err != nil { agg = errors.Join(agg, err) } } return agg } -// RegisterAction adds a single IPC handler. -func (b *Ipc) RegisterAction(handler func(*Core, Message) error) { - b.ipcMu.Lock() - b.ipcHandlers = append(b.ipcHandlers, handler) - b.ipcMu.Unlock() -} - -// RegisterActions adds multiple IPC handlers. -func (b *Ipc) RegisterActions(handlers ...func(*Core, Message) error) { - b.ipcMu.Lock() - b.ipcHandlers = append(b.ipcHandlers, handlers...) - b.ipcMu.Unlock() -} - -// Query dispatches a query to handlers until one responds. -func (b *Ipc) Query(q Query) (any, bool, error) { - b.queryMu.RLock() - handlers := slices.Clone(b.queryHandlers) - b.queryMu.RUnlock() +func (c *Core) Query(q Query) (any, bool, error) { + c.ipc.queryMu.RLock() + handlers := slices.Clone(c.ipc.queryHandlers) + c.ipc.queryMu.RUnlock() for _, h := range handlers { - result, handled, err := h(b.core, q) + result, handled, err := h(c, q) if handled { return result, true, err } @@ -75,16 +52,15 @@ func (b *Ipc) Query(q Query) (any, bool, error) { return nil, false, nil } -// QueryAll dispatches a query to all handlers and collects all responses. -func (b *Ipc) QueryAll(q Query) ([]any, error) { - b.queryMu.RLock() - handlers := slices.Clone(b.queryHandlers) - b.queryMu.RUnlock() +func (c *Core) QueryAll(q Query) ([]any, error) { + c.ipc.queryMu.RLock() + handlers := slices.Clone(c.ipc.queryHandlers) + c.ipc.queryMu.RUnlock() var results []any var agg error for _, h := range handlers { - result, handled, err := h(b.core, q) + result, handled, err := h(c, q) if err != nil { agg = errors.Join(agg, err) } @@ -95,31 +71,8 @@ func (b *Ipc) QueryAll(q Query) ([]any, error) { return results, agg } -// RegisterQuery adds a query handler. -func (b *Ipc) RegisterQuery(handler QueryHandler) { - b.queryMu.Lock() - b.queryHandlers = append(b.queryHandlers, handler) - b.queryMu.Unlock() -} - -// Perform dispatches a task to handlers until one executes it. -func (b *Ipc) Perform(t Task) (any, bool, error) { - b.taskMu.RLock() - handlers := slices.Clone(b.taskHandlers) - b.taskMu.RUnlock() - - for _, h := range handlers { - result, handled, err := h(b.core, t) - if handled { - return result, true, err - } - } - return nil, false, nil -} - -// RegisterTask adds a task handler. -func (b *Ipc) RegisterTask(handler TaskHandler) { - b.taskMu.Lock() - b.taskHandlers = append(b.taskHandlers, handler) - b.taskMu.Unlock() +func (c *Core) RegisterQuery(handler QueryHandler) { + c.ipc.queryMu.Lock() + c.ipc.queryHandlers = append(c.ipc.queryHandlers, handler) + c.ipc.queryMu.Unlock() } diff --git a/pkg/core/service.go b/pkg/core/service.go index 3b489d4..526b755 100644 --- a/pkg/core/service.go +++ b/pkg/core/service.go @@ -17,12 +17,6 @@ type Service struct { locked bool } -// NewService creates an empty service registry. -func NewService() *Service { - return &Service{ - Services: make(map[string]any), - } -} // --- Core service methods --- @@ -58,6 +52,9 @@ func (c *Core) Service(args ...any) any { return E("core.Service", fmt.Sprintf("service %q already registered", name), nil) } svc := args[1] + if c.srv.Services == nil { + c.srv.Services = make(map[string]any) + } c.srv.Services[name] = svc if st, ok := svc.(Startable); ok { c.srv.startables = append(c.srv.startables, st) diff --git a/pkg/core/task.go b/pkg/core/task.go index 542ec7e..cc6e308 100644 --- a/pkg/core/task.go +++ b/pkg/core/task.go @@ -4,7 +4,10 @@ package core -import "fmt" +import ( + "fmt" + "slices" +) // TaskState holds background task state. type TaskState struct { @@ -38,3 +41,23 @@ func (c *Core) PerformAsync(t Task) string { func (c *Core) Progress(taskID string, progress float64, message string, t Task) { _ = c.ACTION(ActionTaskProgress{TaskID: taskID, Task: t, Progress: progress, Message: message}) } + +func (c *Core) Perform(t Task) (any, bool, error) { + c.ipc.taskMu.RLock() + handlers := slices.Clone(c.ipc.taskHandlers) + c.ipc.taskMu.RUnlock() + + for _, h := range handlers { + result, handled, err := h(c, t) + if handled { + return result, true, err + } + } + return nil, false, nil +} + +func (c *Core) RegisterTask(handler TaskHandler) { + c.ipc.taskMu.Lock() + c.ipc.taskHandlers = append(c.ipc.taskHandlers, handler) + c.ipc.taskMu.Unlock() +}