diff --git a/pkg/core/command.go b/pkg/core/command.go index e8d7a90..5610dd4 100644 --- a/pkg/core/command.go +++ b/pkg/core/command.go @@ -7,7 +7,7 @@ // Register a command: // // c.Command("deploy", func(opts core.Options) core.Result { -// return core.Result{Value: "deployed", OK: true} +// return core.Result{"deployed", true} // }) // // Register a nested command: @@ -138,11 +138,11 @@ func (c *Core) Command(path string, command ...Command) Result { c.commands.mu.RLock() cmd, ok := c.commands.commands[path] c.commands.mu.RUnlock() - return Result{Value: cmd, OK: ok} + return Result{cmd, ok} } if path == "" { - return Result{Value: E("core.Command", "command path cannot be empty", nil)} + return Result{E("core.Command", "command path cannot be empty", nil), false} } c.commands.mu.Lock() diff --git a/pkg/core/data.go b/pkg/core/data.go index 7a06cb3..d46c21e 100644 --- a/pkg/core/data.go +++ b/pkg/core/data.go @@ -71,12 +71,12 @@ func (d *Data) New(opts Options) Result { r := Mount(fsys, path) if !r.OK { - return Result{} + return r } emb := r.Value.(*Embed) d.mounts[name] = emb - return Result{Value: emb, OK: true} + return Result{emb, true} } // Get returns the Embed for a named mount point. @@ -128,7 +128,7 @@ func (d *Data) ReadString(path string) Result { if !r.OK { return r } - return Result{Value: string(r.Value.([]byte)), OK: true} + return Result{string(r.Value.([]byte)), true} } // List returns directory entries at a path. @@ -142,9 +142,9 @@ func (d *Data) List(path string) Result { } entries, err := emb.ReadDir(rel) if err != nil { - return Result{} + return Result{err, false} } - return Result{Value: entries, OK: true} + return Result{entries, true} } // ListNames returns filenames (without extensions) at a path. @@ -165,7 +165,7 @@ func (d *Data) ListNames(path string) Result { } names = append(names, name) } - return Result{Value: names, OK: true} + return Result{names, true} } // Extract copies a template directory to targetDir. diff --git a/pkg/core/drive.go b/pkg/core/drive.go index 833c3c3..056b5b3 100644 --- a/pkg/core/drive.go +++ b/pkg/core/drive.go @@ -69,7 +69,7 @@ func (d *Drive) New(opts Options) Result { } d.handles[name] = handle - return Result{Value: handle, OK: true} + return Result{handle, true} } // Get returns a handle by name. diff --git a/pkg/core/embed.go b/pkg/core/embed.go index 6f54497..72d419b 100644 --- a/pkg/core/embed.go +++ b/pkg/core/embed.go @@ -34,7 +34,6 @@ import ( "io/fs" "os" "path/filepath" - "strings" "sync" "text/template" ) @@ -81,9 +80,9 @@ func GetAsset(group, name string) Result { } s, err := decompress(data) if err != nil { - return Result{}.Result(nil, err) + return Result{err, false} } - return Result{}.Result(s) + return Result{s, true} } // GetAssetBytes retrieves a packed asset as bytes. @@ -95,7 +94,7 @@ func GetAssetBytes(group, name string) Result { if !r.OK { return r } - return Result{}.Result([]byte(r.Value.(string))) + return Result{[]byte(r.Value.(string)), true} } // --- Build-time: AST Scanner --- @@ -126,7 +125,7 @@ func ScanAssets(filenames []string) Result { fset := token.NewFileSet() node, err := parser.ParseFile(fset, filename, nil, parser.AllErrors) if err != nil { - return Result{}.Result(nil, err) + return Result{err, false} } baseDir := filepath.Dir(filename) @@ -202,7 +201,7 @@ func ScanAssets(filenames []string) Result { return true }) if scanErr != nil { - return Result{}.Result(nil, scanErr) + return Result{scanErr, false} } } @@ -210,18 +209,18 @@ func ScanAssets(filenames []string) Result { for _, pkg := range packageMap { result = append(result, *pkg) } - return Result{}.Result(result) + return Result{result, true} } // GeneratePack creates Go source code that embeds the scanned assets. func GeneratePack(pkg ScannedPackage) Result { - var b strings.Builder + b := NewBuilder() b.WriteString(fmt.Sprintf("package %s\n\n", pkg.PackageName)) b.WriteString("// Code generated by core pack. DO NOT EDIT.\n\n") if len(pkg.Assets) == 0 && len(pkg.Groups) == 0 { - return Result{}.Result(b.String()) + return Result{b.String(), true} } b.WriteString("import \"forge.lthn.ai/core/go/pkg/core\"\n\n") @@ -232,7 +231,7 @@ func GeneratePack(pkg ScannedPackage) Result { for _, groupPath := range pkg.Groups { files, err := getAllFiles(groupPath) if err != nil { - return Result{}.Result(nil, err) + return Result{err, false} } for _, file := range files { if packed[file] { @@ -240,12 +239,12 @@ func GeneratePack(pkg ScannedPackage) Result { } data, err := compressFile(file) if err != nil { - return Result{}.Result(nil, err) + return Result{err, false} } localPath := TrimPrefix(file, groupPath+"/") relGroup, err := filepath.Rel(pkg.BaseDir, groupPath) if err != nil { - return Result{}.Result(nil, err) + return Result{err, false} } b.WriteString(fmt.Sprintf("\tcore.AddAsset(%q, %q, %q)\n", relGroup, localPath, data)) packed[file] = true @@ -259,14 +258,14 @@ func GeneratePack(pkg ScannedPackage) Result { } data, err := compressFile(asset.FullPath) if err != nil { - return Result{}.Result(nil, err) + return Result{err, false} } b.WriteString(fmt.Sprintf("\tcore.AddAsset(%q, %q, %q)\n", asset.Group, asset.Name, data)) packed[asset.FullPath] = true } b.WriteString("}\n") - return Result{}.Result(b.String()) + return Result{b.String(), true} } // --- Compression --- @@ -302,7 +301,7 @@ func compress(input string) (string, error) { } func decompress(input string) (string, error) { - b64 := base64.NewDecoder(base64.StdEncoding, strings.NewReader(input)) + b64 := base64.NewDecoder(base64.StdEncoding, NewReader(input)) gz, err := gzip.NewReader(b64) if err != nil { return "", err @@ -354,9 +353,9 @@ func Mount(fsys fs.FS, basedir string) Result { } if _, err := s.ReadDir("."); err != nil { - return Result{}.Result(nil, err) + return Result{err, false} } - return Result{}.Result(s) + return Result{s, true} } // MountEmbed creates a scoped view of an embed.FS. @@ -377,9 +376,9 @@ func (s *Embed) path(name string) string { func (s *Embed) Open(name string) Result { f, err := s.fsys.Open(s.path(name)) if err != nil { - return Result{}.Result(nil, err) + return Result{err, false} } - return Result{}.Result(f) + return Result{f, true} } // ReadDir reads the named directory. @@ -394,9 +393,9 @@ func (s *Embed) ReadDir(name string) ([]fs.DirEntry, error) { func (s *Embed) ReadFile(name string) Result { data, err := fs.ReadFile(s.fsys, s.path(name)) if err != nil { - return Result{}.Result(nil, err) + return Result{err, false} } - return Result{}.Result(data) + return Result{data, true} } // ReadString reads the named file as a string. @@ -408,7 +407,7 @@ func (s *Embed) ReadString(name string) Result { if !r.OK { return r } - return Result{}.Result(string(r.Value.([]byte))) + return Result{string(r.Value.([]byte)), true} } // Sub returns a new Embed anchored at a subdirectory within this mount. @@ -418,9 +417,9 @@ func (s *Embed) ReadString(name string) Result { func (s *Embed) Sub(subDir string) Result { sub, err := fs.Sub(s.fsys, s.path(subDir)) if err != nil { - return Result{}.Result(nil, err) + return Result{err, false} } - return Result{}.Result(&Embed{fsys: sub, basedir: "."}) + return Result{&Embed{fsys: sub, basedir: "."}, true} } // FS returns the underlying fs.FS. @@ -489,10 +488,10 @@ func Extract(fsys fs.FS, targetDir string, data any, opts ...ExtractOptions) Res // Ensure target directory exists targetDir, err := filepath.Abs(targetDir) if err != nil { - return Result{}.Result(nil, err) + return Result{err, false} } if err := os.MkdirAll(targetDir, 0755); err != nil { - return Result{}.Result(nil, err) + return Result{err, false} } // Categorise files @@ -523,14 +522,14 @@ func Extract(fsys fs.FS, targetDir string, data any, opts ...ExtractOptions) Res return nil }) if err != nil { - return Result{}.Result(nil, err) + return Result{err, false} } // Create directories (names may contain templates) for _, dir := range dirs { target := renderPath(filepath.Join(targetDir, dir), data) if err := os.MkdirAll(target, 0755); err != nil { - return Result{}.Result(nil, err) + return Result{err, false} } } @@ -538,7 +537,7 @@ func Extract(fsys fs.FS, targetDir string, data any, opts ...ExtractOptions) Res for _, path := range templateFiles { tmpl, err := template.ParseFS(fsys, path) if err != nil { - return Result{}.Result(nil, err) + return Result{err, false} } targetFile := renderPath(filepath.Join(targetDir, path), data) @@ -556,11 +555,11 @@ func Extract(fsys fs.FS, targetDir string, data any, opts ...ExtractOptions) Res f, err := os.Create(targetFile) if err != nil { - return Result{}.Result(nil, err) + return Result{err, false} } if err := tmpl.Execute(f, data); err != nil { f.Close() - return Result{}.Result(nil, err) + return Result{err, false} } f.Close() } @@ -574,7 +573,7 @@ func Extract(fsys fs.FS, targetDir string, data any, opts ...ExtractOptions) Res } target := renderPath(filepath.Join(targetDir, targetPath), data) if err := copyFile(fsys, path, target); err != nil { - return Result{}.Result(nil, err) + return Result{err, false} } } diff --git a/pkg/core/error.go b/pkg/core/error.go index ca616b0..399288d 100644 --- a/pkg/core/error.go +++ b/pkg/core/error.go @@ -156,9 +156,9 @@ func Op(err error) string { return "" } -// ErrCode extracts the error code from an error. +// ErrorCode extracts the error code from an error. // Returns empty string if the error is not an *Err or has no code. -func ErrCode(err error) string { +func ErrorCode(err error) string { var e *Err if As(err, &e) { return e.Code diff --git a/pkg/core/fs.go b/pkg/core/fs.go index b610540..fc8efd6 100644 --- a/pkg/core/fs.go +++ b/pkg/core/fs.go @@ -44,7 +44,7 @@ func (m *Fs) path(p string) string { // validatePath ensures the path is within the sandbox, following symlinks if they exist. func (m *Fs) validatePath(p string) Result { if m.root == "/" { - return Result{Value: m.path(p), OK: true} + return Result{m.path(p), true} } // Split the cleaned path into components @@ -66,7 +66,7 @@ func (m *Fs) validatePath(p string) Result { current = next continue } - return Result{} + return Result{err, false} } // Verify the resolved part is still within the root @@ -79,12 +79,12 @@ func (m *Fs) validatePath(p string) Result { } Print(os.Stderr, "[%s] SECURITY sandbox escape detected root=%s path=%s attempted=%s user=%s", time.Now().Format(time.RFC3339), m.root, p, realNext, username) - return Result{} + return Result{err, false} } current = realNext } - return Result{Value: current, OK: true} + return Result{current, true} } // Read returns file contents as string. @@ -93,14 +93,11 @@ func (m *Fs) Read(p string) Result { if !vp.OK { return Result{} } - r := &Result{} data, err := os.ReadFile(vp.Value.(string)) if err != nil { - return Result{} + return Result{err, false} } - r.Value = string(data) - r.OK = true - return *r + return Result{string(data), true} } // Write saves content to file, creating parent directories as needed. @@ -115,28 +112,28 @@ func (m *Fs) Write(p, content string) Result { func (m *Fs) WriteMode(p, content string, mode os.FileMode) Result { vp := m.validatePath(p) if !vp.OK { - return Result{} + return vp } full := vp.Value.(string) if err := os.MkdirAll(filepath.Dir(full), 0755); err != nil { - return Result{} + return Result{err, false} } if err := os.WriteFile(full, []byte(content), mode); err != nil { - return Result{} + return Result{err, false} } - return Result{OK: true} + return Result{nil, true} } // EnsureDir creates directory if it doesn't exist. func (m *Fs) EnsureDir(p string) Result { vp := m.validatePath(p) if !vp.OK { - return Result{} + return vp } if err := os.MkdirAll(vp.Value.(string), 0755); err != nil { - return Result{} + return Result{err, false} } - return Result{OK: true} + return Result{nil, true} } // IsDir returns true if path is a directory. @@ -206,11 +203,11 @@ func (m *Fs) Open(p string) Result { func (m *Fs) Create(p string) Result { vp := m.validatePath(p) if !vp.OK { - return Result{} + return vp } full := vp.Value.(string) if err := os.MkdirAll(filepath.Dir(full), 0755); err != nil { - return Result{} + return Result{err, false} } return Result{}.Result(os.Create(full)) } @@ -219,11 +216,11 @@ func (m *Fs) Create(p string) Result { func (m *Fs) Append(p string) Result { vp := m.validatePath(p) if !vp.OK { - return Result{} + return vp } full := vp.Value.(string) if err := os.MkdirAll(filepath.Dir(full), 0755); err != nil { - return Result{} + return Result{err, false} } return Result{}.Result(os.OpenFile(full, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)) } @@ -242,32 +239,32 @@ func (m *Fs) WriteStream(path string) Result { func (m *Fs) Delete(p string) Result { vp := m.validatePath(p) if !vp.OK { - return Result{} + return vp } full := vp.Value.(string) if full == "/" || full == os.Getenv("HOME") { return Result{} } if err := os.Remove(full); err != nil { - return Result{} + return Result{err, false} } - return Result{OK: true} + return Result{nil, true} } // DeleteAll removes a file or directory recursively. func (m *Fs) DeleteAll(p string) Result { vp := m.validatePath(p) if !vp.OK { - return Result{} + return vp } full := vp.Value.(string) if full == "/" || full == os.Getenv("HOME") { return Result{} } if err := os.RemoveAll(full); err != nil { - return Result{} + return Result{err, false} } - return Result{OK: true} + return Result{nil, true} } // Rename moves a file or directory. @@ -281,7 +278,7 @@ func (m *Fs) Rename(oldPath, newPath string) Result { return Result{} } if err := os.Rename(oldVp.Value.(string), newVp.Value.(string)); err != nil { - return Result{} + return Result{err, false} } - return Result{OK: true} + return Result{nil, true} } diff --git a/pkg/core/ipc.go b/pkg/core/ipc.go index 9a4d958..5f22c6f 100644 --- a/pkg/core/ipc.go +++ b/pkg/core/ipc.go @@ -62,7 +62,7 @@ func (c *Core) QueryAll(q Query) Result { results = append(results, r.Value) } } - return Result{Value: results, OK: true} + return Result{results, true} } func (c *Core) RegisterQuery(handler QueryHandler) { diff --git a/pkg/core/log.go b/pkg/core/log.go index 9c5dcc0..3faf6ee 100644 --- a/pkg/core/log.go +++ b/pkg/core/log.go @@ -362,32 +362,32 @@ func (le *LogErr) Log(err error) { if err == nil { return } - le.log.Error(ErrorMessage(err), "op", Op(err), "code", ErrCode(err), "stack", FormatStackTrace(err)) + le.log.Error(ErrorMessage(err), "op", Op(err), "code", ErrorCode(err), "stack", FormatStackTrace(err)) } -// --- LogPan: Panic-Aware Logger --- +// --- LogPanic: Panic-Aware Logger --- -// LogPan logs panic context without crash file management. +// LogPanic logs panic context without crash file management. // Primary action: log. Secondary: recover panics. -type LogPan struct { +type LogPanic struct { log *Log } -// NewLogPan creates a LogPan bound to the given logger. -func NewLogPan(log *Log) *LogPan { - return &LogPan{log: log} +// NewLogPanic creates a LogPanic bound to the given logger. +func NewLogPanic(log *Log) *LogPanic { + return &LogPanic{log: log} } // Recover captures a panic and logs it. Does not write crash files. -// Use as: defer core.NewLogPan(logger).Recover() -func (lp *LogPan) Recover() { +// Use as: defer core.NewLogPanic(logger).Recover() +func (lp *LogPanic) Recover() { r := recover() if r == nil { return } err, ok := r.(error) if !ok { - err = fmt.Errorf("%v", r) + err = NewError(fmt.Sprint("panic: ", r)) } lp.log.Error("panic recovered", "err", err, diff --git a/pkg/core/runtime.go b/pkg/core/runtime.go index 5e2aebb..d723e5a 100644 --- a/pkg/core/runtime.go +++ b/pkg/core/runtime.go @@ -35,7 +35,7 @@ func (r *ServiceRuntime[T]) Config() *Config { return r.core.Config() } func (c *Core) ServiceStartup(ctx context.Context, options any) Result { for _, s := range c.Startables() { if err := ctx.Err(); err != nil { - return Result{Value: err} + return Result{err, false} } r := s.OnStart() if !r.OK { @@ -52,7 +52,7 @@ func (c *Core) ServiceShutdown(ctx context.Context) Result { c.ACTION(ActionServiceShutdown{}) for _, s := range c.Stoppables() { if err := ctx.Err(); err != nil { - return Result{Value: err} + return Result{err, false} } s.OnStop() } @@ -64,7 +64,7 @@ func (c *Core) ServiceShutdown(ctx context.Context) Result { select { case <-done: case <-ctx.Done(): - return Result{Value: ctx.Err()} + return Result{ctx.Err(), false} } return Result{OK: true} } @@ -99,7 +99,7 @@ func NewWithFactories(app any, factories map[string]ServiceFactory) Result { c.Service(name, svc) } } - return Result{Value: &Runtime{app: app, Core: c}, OK: true} + return Result{&Runtime{app: app, Core: c}, true} } // NewRuntime creates a Runtime with no custom services. diff --git a/pkg/core/service.go b/pkg/core/service.go index 072e1e3..c938a67 100644 --- a/pkg/core/service.go +++ b/pkg/core/service.go @@ -46,21 +46,21 @@ func (c *Core) Service(name string, service ...Service) Result { c.Lock("srv").Mu.RLock() v, ok := c.services.services[name] c.Lock("srv").Mu.RUnlock() - return Result{Value: v, OK: ok} + return Result{v, ok} } if name == "" { - return Result{Value: E("core.Service", "service name cannot be empty", nil)} + return Result{E("core.Service", "service name cannot be empty", nil), false} } c.Lock("srv").Mu.Lock() defer c.Lock("srv").Mu.Unlock() if c.services.locked { - return Result{Value: E("core.Service", Concat("service \"", name, "\" not permitted — registry locked"), nil)} + return Result{E("core.Service", Concat("service \"", name, "\" not permitted — registry locked"), nil), false} } if _, exists := c.services.services[name]; exists { - return Result{Value: E("core.Service", Join(" ", "service", name, "already registered"), nil)} + return Result{E("core.Service", Join(" ", "service", name, "already registered"), nil), false} } srv := &service[0] diff --git a/pkg/core/string.go b/pkg/core/string.go index af51948..02db67f 100644 --- a/pkg/core/string.go +++ b/pkg/core/string.go @@ -111,13 +111,29 @@ func RuneCount(s string) int { return utf8.RuneCountInString(s) } +// NewBuilder returns a new strings.Builder. +// +// b := core.NewBuilder() +// b.WriteString("hello") +// b.String() // "hello" +func NewBuilder() *strings.Builder { + return &strings.Builder{} +} + +// NewReader returns a strings.NewReader for the given string. +// +// r := core.NewReader("hello world") +func NewReader(s string) *strings.Reader { + return strings.NewReader(s) +} + // Concat joins variadic string parts into one string. // Hook point for validation, sanitisation, and security checks. // // core.Concat("cmd.", "deploy.to.homelab", ".description") // core.Concat("https://", host, "/api/v1") func Concat(parts ...string) string { - var b strings.Builder + b := NewBuilder() for _, p := range parts { b.WriteString(p) } diff --git a/pkg/core/utils.go b/pkg/core/utils.go index df21006..038e32e 100644 --- a/pkg/core/utils.go +++ b/pkg/core/utils.go @@ -51,13 +51,13 @@ func Arg(index int, args ...any) Result { v := args[index] switch v.(type) { case string: - return Result{Value: ArgString(index, args...), OK: true} + return Result{ArgString(index, args...), true} case int: - return Result{Value: ArgInt(index, args...), OK: true} + return Result{ArgInt(index, args...), true} case bool: - return Result{Value: ArgBool(index, args...), OK: true} + return Result{ArgBool(index, args...), true} default: - return Result{Value: v, OK: true} + return Result{v, true} } } diff --git a/tests/error_test.go b/tests/error_test.go index 6a58382..758a5fe 100644 --- a/tests/error_test.go +++ b/tests/error_test.go @@ -39,13 +39,13 @@ func TestWrapCode_Good(t *testing.T) { cause := errors.New("invalid email") err := WrapCode(cause, "VALIDATION_ERROR", "user.Validate", "bad input") assert.Error(t, err) - assert.Equal(t, "VALIDATION_ERROR", ErrCode(err)) + assert.Equal(t, "VALIDATION_ERROR", ErrorCode(err)) } func TestNewCode_Good(t *testing.T) { err := NewCode("NOT_FOUND", "resource not found") assert.Error(t, err) - assert.Equal(t, "NOT_FOUND", ErrCode(err)) + assert.Equal(t, "NOT_FOUND", ErrorCode(err)) } // --- Error Introspection --- diff --git a/tests/log_test.go b/tests/log_test.go index a2e4065..a586185 100644 --- a/tests/log_test.go +++ b/tests/log_test.go @@ -120,17 +120,17 @@ func TestLogErr_Nil_Good(t *testing.T) { le.Log(nil) // should not panic } -// --- LogPan --- +// --- LogPanic --- -func TestLogPan_Good(t *testing.T) { +func TestLogPanic_Good(t *testing.T) { l := NewLog(LogOptions{Level: LevelInfo}) - lp := NewLogPan(l) + lp := NewLogPanic(l) assert.NotNil(t, lp) } -func TestLogPan_Recover_Good(t *testing.T) { +func TestLogPanic_Recover_Good(t *testing.T) { l := NewLog(LogOptions{Level: LevelInfo}) - lp := NewLogPan(l) + lp := NewLogPanic(l) assert.NotPanics(t, func() { defer lp.Recover() panic("caught")