fix: AX audit round 5 — full naming, Result returns throughout
Renames (via GoLand refactor): - Option.K → Key, Option.V → Value - Err.Op → Operation, Err.Msg → Message, Err.Err → Error - CrashSystem.OS → OperatingSystem, Arch → Architecture - TaskID → TaskIdentifier, TaskWithID → TaskWithIdentifier - Ipc → IPC, BaseDir → BaseDirectory - ServiceRuntime.Opts → Options Return type changes: - Options.Get, Config.Get → Result (was (any, bool)) - Embed.ReadDir → Result (was ([]fs.DirEntry, error)) - Translator.Translate, I18n.Translate → Result (was string) Rule 6: - data.go: propagate opts.Get failure, typed error for bad fs.FS Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
cf25af1a13
commit
298322ed89
24 changed files with 231 additions and 227 deletions
|
|
@ -5,7 +5,7 @@
|
|||
//
|
||||
// Run the CLI:
|
||||
//
|
||||
// c := core.New(core.Options{{K: "name", V: "myapp"}})
|
||||
// c := core.New(core.Options{{Key: "name", Value: "myapp"}})
|
||||
// c.Command("deploy", handler)
|
||||
// c.Cli().Run()
|
||||
//
|
||||
|
|
@ -84,12 +84,12 @@ func (cl *Cli) Run(args ...string) Result {
|
|||
key, val, valid := ParseFlag(arg)
|
||||
if valid {
|
||||
if val != "" {
|
||||
opts = append(opts, Option{K: key, V: val})
|
||||
opts = append(opts, Option{Key: key, Value: val})
|
||||
} else {
|
||||
opts = append(opts, Option{K: key, V: true})
|
||||
opts = append(opts, Option{Key: key, Value: true})
|
||||
}
|
||||
} else if !IsFlag(arg) {
|
||||
opts = append(opts, Option{K: "_arg", V: arg})
|
||||
opts = append(opts, Option{Key: "_arg", Value: arg})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -121,7 +121,7 @@ func (cl *Cli) PrintHelp() {
|
|||
if cmd.Hidden {
|
||||
continue
|
||||
}
|
||||
desc := cl.core.I18n().Translate(cmd.I18nKey())
|
||||
desc := cl.core.I18n().Translate(cmd.I18nKey()).Value.(string)
|
||||
if desc == cmd.I18nKey() {
|
||||
cl.Print(" %s", path)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -43,13 +43,13 @@ type CommandLifecycle interface {
|
|||
// Command is the DTO for an executable operation.
|
||||
type Command struct {
|
||||
Name string
|
||||
Description string // i18n key — derived from path if empty
|
||||
Path string // "deploy/to/homelab"
|
||||
Action CommandAction // business logic
|
||||
Lifecycle CommandLifecycle // optional — provided by go-process
|
||||
Flags Options // declared flags
|
||||
Description string // i18n key — derived from path if empty
|
||||
Path string // "deploy/to/homelab"
|
||||
Action CommandAction // business logic
|
||||
Lifecycle CommandLifecycle // optional — provided by go-process
|
||||
Flags Options // declared flags
|
||||
Hidden bool
|
||||
commands map[string]*Command // child commands (internal)
|
||||
commands map[string]*Command // child commands (internal)
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ func (cmd *Command) I18nKey() string {
|
|||
|
||||
// Run executes the command's action with the given options.
|
||||
//
|
||||
// result := cmd.Run(core.Options{{K: "target", V: "homelab"}})
|
||||
// result := cmd.Run(core.Options{{Key: "target", Value: "homelab"}})
|
||||
func (cmd *Command) Run(opts Options) Result {
|
||||
if cmd.Action == nil {
|
||||
return Result{}
|
||||
|
|
|
|||
|
|
@ -57,14 +57,17 @@ func (e *Config) Set(key string, val any) {
|
|||
}
|
||||
|
||||
// Get retrieves a configuration value by key.
|
||||
func (e *Config) Get(key string) (any, bool) {
|
||||
func (e *Config) Get(key string) Result {
|
||||
e.mu.RLock()
|
||||
defer e.mu.RUnlock()
|
||||
if e.ConfigOptions == nil || e.Settings == nil {
|
||||
return nil, false
|
||||
return Result{}
|
||||
}
|
||||
val, ok := e.Settings[key]
|
||||
return val, ok
|
||||
if !ok {
|
||||
return Result{}
|
||||
}
|
||||
return Result{val, true}
|
||||
}
|
||||
|
||||
func (e *Config) String(key string) string { return ConfigGet[string](e, key) }
|
||||
|
|
@ -73,12 +76,12 @@ func (e *Config) Bool(key string) bool { return ConfigGet[bool](e, key) }
|
|||
|
||||
// ConfigGet retrieves a typed configuration value.
|
||||
func ConfigGet[T any](e *Config, key string) T {
|
||||
val, ok := e.Get(key)
|
||||
if !ok {
|
||||
r := e.Get(key)
|
||||
if !r.OK {
|
||||
var zero T
|
||||
return zero
|
||||
}
|
||||
typed, _ := val.(T)
|
||||
typed, _ := r.Value.(T)
|
||||
return typed
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ type Task any
|
|||
// TaskWithID is an optional interface for tasks that need to know their assigned ID.
|
||||
type TaskWithID interface {
|
||||
Task
|
||||
SetTaskID(id string)
|
||||
GetTaskID() string
|
||||
TaskWithIdentifier(id string)
|
||||
GetTaskIdentifier() string
|
||||
}
|
||||
|
||||
// QueryHandler handles Query requests. Returns Result{Value, OK}.
|
||||
|
|
@ -46,22 +46,22 @@ type ActionServiceStartup struct{}
|
|||
type ActionServiceShutdown struct{}
|
||||
|
||||
type ActionTaskStarted struct {
|
||||
TaskID string
|
||||
Task Task
|
||||
TaskIdentifier string
|
||||
Task Task
|
||||
}
|
||||
|
||||
type ActionTaskProgress struct {
|
||||
TaskID string
|
||||
Task Task
|
||||
Progress float64
|
||||
Message string
|
||||
TaskIdentifier string
|
||||
Task Task
|
||||
Progress float64
|
||||
Message string
|
||||
}
|
||||
|
||||
type ActionTaskCompleted struct {
|
||||
TaskID string
|
||||
Task Task
|
||||
Result any
|
||||
Error error
|
||||
TaskIdentifier string
|
||||
Task Task
|
||||
Result any
|
||||
Error error
|
||||
}
|
||||
|
||||
// --- Constructor ---
|
||||
|
|
@ -69,20 +69,20 @@ type ActionTaskCompleted struct {
|
|||
// New creates a Core instance.
|
||||
//
|
||||
// c := core.New(core.Options{
|
||||
// {K: "name", V: "myapp"},
|
||||
// {Key: "name", Value: "myapp"},
|
||||
// })
|
||||
func New(opts ...Options) *Core {
|
||||
c := &Core{
|
||||
app: &App{},
|
||||
data: &Data{},
|
||||
drive: &Drive{},
|
||||
fs: &Fs{root: "/"},
|
||||
config: &Config{ConfigOptions: &ConfigOptions{}},
|
||||
error: &ErrorPanic{},
|
||||
log: &ErrorLog{log: defaultLog},
|
||||
lock: &Lock{},
|
||||
ipc: &Ipc{},
|
||||
i18n: &I18n{},
|
||||
app: &App{},
|
||||
data: &Data{},
|
||||
drive: &Drive{},
|
||||
fs: &Fs{root: "/"},
|
||||
config: &Config{ConfigOptions: &ConfigOptions{}},
|
||||
error: &ErrorPanic{},
|
||||
log: &ErrorLog{log: defaultLog},
|
||||
lock: &Lock{},
|
||||
ipc: &Ipc{},
|
||||
i18n: &I18n{},
|
||||
}
|
||||
|
||||
if len(opts) > 0 {
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@
|
|||
// Mount a package's assets:
|
||||
//
|
||||
// c.Data().New(core.Options{
|
||||
// {K: "name", V: "brain"},
|
||||
// {K: "source", V: brainFS},
|
||||
// {K: "path", V: "prompts"},
|
||||
// {Key: "name", Value: "brain"},
|
||||
// {Key: "source", Value: brainFS},
|
||||
// {Key: "path", Value: "prompts"},
|
||||
// })
|
||||
//
|
||||
// Read from any mounted path:
|
||||
|
|
@ -37,9 +37,9 @@ type Data struct {
|
|||
// New registers an embedded filesystem under a named prefix.
|
||||
//
|
||||
// c.Data().New(core.Options{
|
||||
// {K: "name", V: "brain"},
|
||||
// {K: "source", V: brainFS},
|
||||
// {K: "path", V: "prompts"},
|
||||
// {Key: "name", Value: "brain"},
|
||||
// {Key: "source", Value: brainFS},
|
||||
// {Key: "path", Value: "prompts"},
|
||||
// })
|
||||
func (d *Data) New(opts Options) Result {
|
||||
name := opts.String("name")
|
||||
|
|
@ -47,14 +47,14 @@ func (d *Data) New(opts Options) Result {
|
|||
return Result{}
|
||||
}
|
||||
|
||||
source, ok := opts.Get("source")
|
||||
if !ok {
|
||||
return Result{}
|
||||
r := opts.Get("source")
|
||||
if !r.OK {
|
||||
return r
|
||||
}
|
||||
|
||||
fsys, ok := source.(fs.FS)
|
||||
fsys, ok := r.Value.(fs.FS)
|
||||
if !ok {
|
||||
return Result{}
|
||||
return Result{E("data.New", "source is not fs.FS", nil), false}
|
||||
}
|
||||
|
||||
path := opts.String("path")
|
||||
|
|
@ -69,12 +69,12 @@ func (d *Data) New(opts Options) Result {
|
|||
d.mounts = make(map[string]*Embed)
|
||||
}
|
||||
|
||||
r := Mount(fsys, path)
|
||||
if !r.OK {
|
||||
return r
|
||||
mr := Mount(fsys, path)
|
||||
if !mr.OK {
|
||||
return mr
|
||||
}
|
||||
|
||||
emb := r.Value.(*Embed)
|
||||
emb := mr.Value.(*Embed)
|
||||
d.mounts[name] = emb
|
||||
return Result{emb, true}
|
||||
}
|
||||
|
|
@ -140,11 +140,11 @@ func (d *Data) List(path string) Result {
|
|||
if emb == nil {
|
||||
return Result{}
|
||||
}
|
||||
entries, err := emb.ReadDir(rel)
|
||||
if err != nil {
|
||||
return Result{err, false}
|
||||
r := emb.ReadDir(rel)
|
||||
if !r.OK {
|
||||
return r
|
||||
}
|
||||
return Result{entries, true}
|
||||
return Result{r.Value, true}
|
||||
}
|
||||
|
||||
// ListNames returns filenames (without extensions) at a path.
|
||||
|
|
|
|||
|
|
@ -7,16 +7,16 @@
|
|||
// Register a transport:
|
||||
//
|
||||
// c.Drive().New(core.Options{
|
||||
// {K: "name", V: "api"},
|
||||
// {K: "transport", V: "https://api.lthn.ai"},
|
||||
// {Key: "name", Value: "api"},
|
||||
// {Key: "transport", Value: "https://api.lthn.ai"},
|
||||
// })
|
||||
// c.Drive().New(core.Options{
|
||||
// {K: "name", V: "ssh"},
|
||||
// {K: "transport", V: "ssh://claude@10.69.69.165"},
|
||||
// {Key: "name", Value: "ssh"},
|
||||
// {Key: "transport", Value: "ssh://claude@10.69.69.165"},
|
||||
// })
|
||||
// c.Drive().New(core.Options{
|
||||
// {K: "name", V: "mcp"},
|
||||
// {K: "transport", V: "mcp://mcp.lthn.sh"},
|
||||
// {Key: "name", Value: "mcp"},
|
||||
// {Key: "transport", Value: "mcp://mcp.lthn.sh"},
|
||||
// })
|
||||
//
|
||||
// Retrieve a handle:
|
||||
|
|
@ -44,8 +44,8 @@ type Drive struct {
|
|||
// New registers a transport handle.
|
||||
//
|
||||
// c.Drive().New(core.Options{
|
||||
// {K: "name", V: "api"},
|
||||
// {K: "transport", V: "https://api.lthn.ai"},
|
||||
// {Key: "name", Value: "api"},
|
||||
// {Key: "transport", Value: "https://api.lthn.ai"},
|
||||
// })
|
||||
func (d *Drive) New(opts Options) Result {
|
||||
name := opts.String("name")
|
||||
|
|
|
|||
|
|
@ -109,10 +109,10 @@ type AssetRef struct {
|
|||
|
||||
// ScannedPackage holds all asset references from a set of source files.
|
||||
type ScannedPackage struct {
|
||||
PackageName string
|
||||
BaseDir string
|
||||
Groups []string
|
||||
Assets []AssetRef
|
||||
PackageName string
|
||||
BaseDirectory string
|
||||
Groups []string
|
||||
Assets []AssetRef
|
||||
}
|
||||
|
||||
// ScanAssets parses Go source files and finds asset references.
|
||||
|
|
@ -131,7 +131,7 @@ func ScanAssets(filenames []string) Result {
|
|||
baseDir := filepath.Dir(filename)
|
||||
pkg, ok := packageMap[baseDir]
|
||||
if !ok {
|
||||
pkg = &ScannedPackage{BaseDir: baseDir}
|
||||
pkg = &ScannedPackage{BaseDirectory: baseDir}
|
||||
packageMap[baseDir] = pkg
|
||||
}
|
||||
pkg.PackageName = node.Name.Name
|
||||
|
|
@ -242,7 +242,7 @@ func GeneratePack(pkg ScannedPackage) Result {
|
|||
return Result{err, false}
|
||||
}
|
||||
localPath := TrimPrefix(file, groupPath+"/")
|
||||
relGroup, err := filepath.Rel(pkg.BaseDir, groupPath)
|
||||
relGroup, err := filepath.Rel(pkg.BaseDirectory, groupPath)
|
||||
if err != nil {
|
||||
return Result{err, false}
|
||||
}
|
||||
|
|
@ -352,8 +352,8 @@ func Mount(fsys fs.FS, basedir string) Result {
|
|||
s.embedFS = &efs
|
||||
}
|
||||
|
||||
if _, err := s.ReadDir("."); err != nil {
|
||||
return Result{err, false}
|
||||
if r := s.ReadDir("."); !r.OK {
|
||||
return r
|
||||
}
|
||||
return Result{s, true}
|
||||
}
|
||||
|
|
@ -382,8 +382,8 @@ func (s *Embed) Open(name string) Result {
|
|||
}
|
||||
|
||||
// ReadDir reads the named directory.
|
||||
func (s *Embed) ReadDir(name string) ([]fs.DirEntry, error) {
|
||||
return fs.ReadDir(s.fsys, s.path(name))
|
||||
func (s *Embed) ReadDir(name string) Result {
|
||||
return Result{}.Result(fs.ReadDir(s.fsys, s.path(name)))
|
||||
}
|
||||
|
||||
// ReadFile reads the named file.
|
||||
|
|
|
|||
|
|
@ -31,28 +31,28 @@ var _ ErrorSink = (*Log)(nil)
|
|||
// Err represents a structured error with operational context.
|
||||
// It implements the error interface and supports unwrapping.
|
||||
type Err struct {
|
||||
Op string // Operation being performed (e.g., "user.Save")
|
||||
Msg string // Human-readable message
|
||||
Err error // Underlying error (optional)
|
||||
Code string // Error code (optional, e.g., "VALIDATION_FAILED")
|
||||
Operation string // Operation being performed (e.g., "user.Save")
|
||||
Message string // Human-readable message
|
||||
Err error // Underlying error (optional)
|
||||
Code string // Error code (optional, e.g., "VALIDATION_FAILED")
|
||||
}
|
||||
|
||||
// Error implements the error interface.
|
||||
func (e *Err) Error() string {
|
||||
var prefix string
|
||||
if e.Op != "" {
|
||||
prefix = e.Op + ": "
|
||||
if e.Operation != "" {
|
||||
prefix = e.Operation + ": "
|
||||
}
|
||||
if e.Err != nil {
|
||||
if e.Code != "" {
|
||||
return Concat(prefix, e.Msg, " [", e.Code, "]: ", e.Err.Error())
|
||||
return Concat(prefix, e.Message, " [", e.Code, "]: ", e.Err.Error())
|
||||
}
|
||||
return Concat(prefix, e.Msg, ": ", e.Err.Error())
|
||||
return Concat(prefix, e.Message, ": ", e.Err.Error())
|
||||
}
|
||||
if e.Code != "" {
|
||||
return Concat(prefix, e.Msg, " [", e.Code, "]")
|
||||
return Concat(prefix, e.Message, " [", e.Code, "]")
|
||||
}
|
||||
return Concat(prefix, e.Msg)
|
||||
return Concat(prefix, e.Message)
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying error for use with errors.Is and errors.As.
|
||||
|
|
@ -70,7 +70,7 @@ func (e *Err) Unwrap() error {
|
|||
// return log.E("user.Save", "failed to save user", err)
|
||||
// return log.E("api.Call", "rate limited", nil) // No underlying cause
|
||||
func E(op, msg string, err error) error {
|
||||
return &Err{Op: op, Msg: msg, Err: err}
|
||||
return &Err{Operation: op, Message: msg, Err: err}
|
||||
}
|
||||
|
||||
// Wrap wraps an error with operation context.
|
||||
|
|
@ -87,9 +87,9 @@ func Wrap(err error, op, msg string) error {
|
|||
// Preserve Code from wrapped *Err
|
||||
var logErr *Err
|
||||
if As(err, &logErr) && logErr.Code != "" {
|
||||
return &Err{Op: op, Msg: msg, Err: err, Code: logErr.Code}
|
||||
return &Err{Operation: op, Message: msg, Err: err, Code: logErr.Code}
|
||||
}
|
||||
return &Err{Op: op, Msg: msg, Err: err}
|
||||
return &Err{Operation: op, Message: msg, Err: err}
|
||||
}
|
||||
|
||||
// WrapCode wraps an error with operation context and error code.
|
||||
|
|
@ -103,7 +103,7 @@ func WrapCode(err error, code, op, msg string) error {
|
|||
if err == nil && code == "" {
|
||||
return nil
|
||||
}
|
||||
return &Err{Op: op, Msg: msg, Err: err, Code: code}
|
||||
return &Err{Operation: op, Message: msg, Err: err, Code: code}
|
||||
}
|
||||
|
||||
// NewCode creates an error with just code and message (no underlying error).
|
||||
|
|
@ -113,7 +113,7 @@ func WrapCode(err error, code, op, msg string) error {
|
|||
//
|
||||
// var ErrNotFound = log.NewCode("NOT_FOUND", "resource not found")
|
||||
func NewCode(code, msg string) error {
|
||||
return &Err{Msg: msg, Code: code}
|
||||
return &Err{Message: msg, Code: code}
|
||||
}
|
||||
|
||||
// --- Standard Library Wrappers ---
|
||||
|
|
@ -150,7 +150,7 @@ func ErrorJoin(errs ...error) error {
|
|||
func Operation(err error) string {
|
||||
var e *Err
|
||||
if As(err, &e) {
|
||||
return e.Op
|
||||
return e.Operation
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
@ -173,7 +173,7 @@ func ErrorMessage(err error) string {
|
|||
}
|
||||
var e *Err
|
||||
if As(err, &e) {
|
||||
return e.Msg
|
||||
return e.Message
|
||||
}
|
||||
return err.Error()
|
||||
}
|
||||
|
|
@ -199,8 +199,8 @@ func AllOperations(err error) iter.Seq[string] {
|
|||
return func(yield func(string) bool) {
|
||||
for err != nil {
|
||||
if e, ok := err.(*Err); ok {
|
||||
if e.Op != "" {
|
||||
if !yield(e.Op) {
|
||||
if e.Operation != "" {
|
||||
if !yield(e.Operation) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -288,9 +288,9 @@ type CrashReport struct {
|
|||
|
||||
// CrashSystem holds system information at crash time.
|
||||
type CrashSystem struct {
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
Version string `json:"go_version"`
|
||||
OperatingSystem string `json:"operatingsystem"`
|
||||
Architecture string `json:"architecture"`
|
||||
Version string `json:"go_version"`
|
||||
}
|
||||
|
||||
// ErrorPanic manages panic recovery and crash reporting.
|
||||
|
|
@ -321,9 +321,9 @@ func (h *ErrorPanic) Recover() {
|
|||
Error: err.Error(),
|
||||
Stack: string(debug.Stack()),
|
||||
System: CrashSystem{
|
||||
OS: runtime.GOOS,
|
||||
Arch: runtime.GOARCH,
|
||||
Version: runtime.Version(),
|
||||
OperatingSystem: runtime.GOOS,
|
||||
Architecture: runtime.GOARCH,
|
||||
Version: runtime.Version(),
|
||||
},
|
||||
Meta: maps.Clone(h.meta),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import (
|
|||
// Implemented by go-i18n's Srv.
|
||||
type Translator interface {
|
||||
// Translate translates a message by its ID with optional arguments.
|
||||
Translate(messageID string, args ...any) string
|
||||
Translate(messageID string, args ...any) Result
|
||||
// SetLanguage sets the active language (BCP47 tag, e.g., "en-GB", "de").
|
||||
SetLanguage(lang string) error
|
||||
// Language returns the current language code.
|
||||
|
|
@ -81,14 +81,14 @@ func (i *I18n) Translator() Translator {
|
|||
}
|
||||
|
||||
// Translate translates a message. Returns the key as-is if no translator is registered.
|
||||
func (i *I18n) Translate(messageID string, args ...any) string {
|
||||
func (i *I18n) Translate(messageID string, args ...any) Result {
|
||||
i.mu.RLock()
|
||||
t := i.translator
|
||||
i.mu.RUnlock()
|
||||
if t != nil {
|
||||
return t.Translate(messageID, args...)
|
||||
}
|
||||
return messageID
|
||||
return Result{messageID, true}
|
||||
}
|
||||
|
||||
// SetLanguage sets the active language. No-op if no translator is registered.
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@
|
|||
// Create options:
|
||||
//
|
||||
// opts := core.Options{
|
||||
// {K: "name", V: "brain"},
|
||||
// {K: "path", V: "prompts"},
|
||||
// {Key: "name", Value: "brain"},
|
||||
// {Key: "path", Value: "prompts"},
|
||||
// }
|
||||
//
|
||||
// Read options:
|
||||
|
|
@ -21,22 +21,22 @@
|
|||
// Use with subsystems:
|
||||
//
|
||||
// c.Drive().New(core.Options{
|
||||
// {K: "name", V: "brain"},
|
||||
// {K: "source", V: brainFS},
|
||||
// {K: "path", V: "prompts"},
|
||||
// {Key: "name", Value: "brain"},
|
||||
// {Key: "source", Value: brainFS},
|
||||
// {Key: "path", Value: "prompts"},
|
||||
// })
|
||||
//
|
||||
// Use with New:
|
||||
//
|
||||
// c := core.New(core.Options{
|
||||
// {K: "name", V: "myapp"},
|
||||
// {Key: "name", Value: "myapp"},
|
||||
// })
|
||||
package core
|
||||
|
||||
// Result is the universal return type for Core operations.
|
||||
// Replaces the (value, error) pattern — errors flow through Core internally.
|
||||
//
|
||||
// r := c.Data().New(core.Options{{K: "name", V: "brain"}})
|
||||
// r := c.Data().New(core.Options{{Key: "name", Value: "brain"}})
|
||||
// if r.OK { use(r.Result()) }
|
||||
type Result struct {
|
||||
Value any
|
||||
|
|
@ -69,49 +69,49 @@ func (r Result) Result(args ...any) Result {
|
|||
|
||||
// Option is a single key-value configuration pair.
|
||||
//
|
||||
// core.Option{K: "name", V: "brain"}
|
||||
// core.Option{K: "port", V: 8080}
|
||||
// core.Option{Key: "name", Value: "brain"}
|
||||
// core.Option{Key: "port", Value: 8080}
|
||||
type Option struct {
|
||||
K string
|
||||
V any
|
||||
Key string
|
||||
Value any
|
||||
}
|
||||
|
||||
// Options is a collection of Option items.
|
||||
// The universal input type for Core operations.
|
||||
//
|
||||
// opts := core.Options{{K: "name", V: "myapp"}}
|
||||
// opts := core.Options{{Key: "name", Value: "myapp"}}
|
||||
// name := opts.String("name")
|
||||
type Options []Option
|
||||
|
||||
// Get retrieves a value by key.
|
||||
//
|
||||
// val, ok := opts.Get("name")
|
||||
func (o Options) Get(key string) (any, bool) {
|
||||
// r := opts.Get("name")
|
||||
// if r.OK { name := r.Value.(string) }
|
||||
func (o Options) Get(key string) Result {
|
||||
for _, opt := range o {
|
||||
if opt.K == key {
|
||||
return opt.V, true
|
||||
if opt.Key == key {
|
||||
return Result{opt.Value, true}
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
return Result{}
|
||||
}
|
||||
|
||||
// Has returns true if a key exists.
|
||||
//
|
||||
// if opts.Has("debug") { ... }
|
||||
func (o Options) Has(key string) bool {
|
||||
_, ok := o.Get(key)
|
||||
return ok
|
||||
return o.Get(key).OK
|
||||
}
|
||||
|
||||
// String retrieves a string value, empty string if missing.
|
||||
//
|
||||
// name := opts.String("name")
|
||||
func (o Options) String(key string) string {
|
||||
val, ok := o.Get(key)
|
||||
if !ok {
|
||||
r := o.Get(key)
|
||||
if !r.OK {
|
||||
return ""
|
||||
}
|
||||
s, _ := val.(string)
|
||||
s, _ := r.Value.(string)
|
||||
return s
|
||||
}
|
||||
|
||||
|
|
@ -119,11 +119,11 @@ func (o Options) String(key string) string {
|
|||
//
|
||||
// port := opts.Int("port")
|
||||
func (o Options) Int(key string) int {
|
||||
val, ok := o.Get(key)
|
||||
if !ok {
|
||||
r := o.Get(key)
|
||||
if !r.OK {
|
||||
return 0
|
||||
}
|
||||
i, _ := val.(int)
|
||||
i, _ := r.Value.(int)
|
||||
return i
|
||||
}
|
||||
|
||||
|
|
@ -131,10 +131,10 @@ func (o Options) Int(key string) int {
|
|||
//
|
||||
// debug := opts.Bool("debug")
|
||||
func (o Options) Bool(key string) bool {
|
||||
val, ok := o.Get(key)
|
||||
if !ok {
|
||||
r := o.Get(key)
|
||||
if !r.OK {
|
||||
return false
|
||||
}
|
||||
b, _ := val.(bool)
|
||||
b, _ := r.Value.(bool)
|
||||
return b
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ func NewServiceRuntime[T any](c *Core, opts T) *ServiceRuntime[T] {
|
|||
}
|
||||
|
||||
func (r *ServiceRuntime[T]) Core() *Core { return r.core }
|
||||
func (r *ServiceRuntime[T]) Options() T { return r.opts }
|
||||
func (r *ServiceRuntime[T]) Options() T { return r.opts }
|
||||
func (r *ServiceRuntime[T]) Config() *Config { return r.core.Config() }
|
||||
|
||||
// --- Lifecycle ---
|
||||
|
|
@ -88,7 +88,7 @@ type ServiceFactory func() Result
|
|||
|
||||
// NewWithFactories creates a Runtime with the provided service factories.
|
||||
func NewWithFactories(app any, factories map[string]ServiceFactory) Result {
|
||||
c := New(Options{{K: "name", V: "core"}})
|
||||
c := New(Options{{Key: "name", Value: "core"}})
|
||||
c.app.Runtime = app
|
||||
|
||||
names := slices.Sorted(maps.Keys(factories))
|
||||
|
|
|
|||
|
|
@ -25,9 +25,9 @@ func (c *Core) PerformAsync(t Task) Result {
|
|||
}
|
||||
taskID := Concat("task-", strconv.FormatUint(c.taskIDCounter.Add(1), 10))
|
||||
if tid, ok := t.(TaskWithID); ok {
|
||||
tid.SetTaskID(taskID)
|
||||
tid.TaskWithIdentifier(taskID)
|
||||
}
|
||||
c.ACTION(ActionTaskStarted{TaskID: taskID, Task: t})
|
||||
c.ACTION(ActionTaskStarted{TaskIdentifier: taskID, Task: t})
|
||||
c.wg.Go(func() {
|
||||
r := c.PERFORM(t)
|
||||
var err error
|
||||
|
|
@ -38,14 +38,14 @@ func (c *Core) PerformAsync(t Task) Result {
|
|||
err = E("core.PerformAsync", Join(" ", "no handler found for task type", reflect.TypeOf(t).String()), nil)
|
||||
}
|
||||
}
|
||||
c.ACTION(ActionTaskCompleted{TaskID: taskID, Task: t, Result: r.Value, Error: err})
|
||||
c.ACTION(ActionTaskCompleted{TaskIdentifier: taskID, Task: t, Result: r.Value, Error: err})
|
||||
})
|
||||
return Result{taskID, true}
|
||||
}
|
||||
|
||||
// Progress broadcasts a progress update for a background task.
|
||||
func (c *Core) Progress(taskID string, progress float64, message string, t Task) {
|
||||
c.ACTION(ActionTaskProgress{TaskID: taskID, Task: t, Progress: progress, Message: message})
|
||||
c.ACTION(ActionTaskProgress{TaskIdentifier: taskID, Task: t, Progress: progress, Message: message})
|
||||
}
|
||||
|
||||
func (c *Core) Perform(t Task) Result {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import (
|
|||
// --- App ---
|
||||
|
||||
func TestApp_Good(t *testing.T) {
|
||||
c := New(Options{{K: "name", V: "myapp"}})
|
||||
c := New(Options{{Key: "name", Value: "myapp"}})
|
||||
assert.Equal(t, "myapp", c.App().Name)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ func TestCli_Good(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCli_Banner_Good(t *testing.T) {
|
||||
c := New(Options{{K: "name", V: "myapp"}})
|
||||
c := New(Options{{Key: "name", Value: "myapp"}})
|
||||
assert.Equal(t, "myapp", c.Cli().Banner())
|
||||
}
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ func TestCli_Run_NoCommand_Good(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCli_PrintHelp_Good(t *testing.T) {
|
||||
c := New(Options{{K: "name", V: "myapp"}})
|
||||
c := New(Options{{Key: "name", Value: "myapp"}})
|
||||
c.Command("deploy", Command{Action: func(_ Options) Result { return Result{OK: true} }})
|
||||
c.Command("serve", Command{Action: func(_ Options) Result { return Result{OK: true} }})
|
||||
c.Cli().PrintHelp()
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ func TestCommand_Run_Good(t *testing.T) {
|
|||
return Result{Value: Concat("hello ", opts.String("name")), OK: true}
|
||||
}})
|
||||
cmd := c.Command("greet").Value.(*Command)
|
||||
r := cmd.Run(Options{{K: "name", V: "world"}})
|
||||
r := cmd.Run(Options{{Key: "name", Value: "world"}})
|
||||
assert.True(t, r.OK)
|
||||
assert.Equal(t, "hello world", r.Value)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,16 +14,16 @@ func TestConfig_SetGet_Good(t *testing.T) {
|
|||
c.Config().Set("api_url", "https://api.lthn.ai")
|
||||
c.Config().Set("max_agents", 5)
|
||||
|
||||
val, ok := c.Config().Get("api_url")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "https://api.lthn.ai", val)
|
||||
r := c.Config().Get("api_url")
|
||||
assert.True(t, r.OK)
|
||||
assert.Equal(t, "https://api.lthn.ai", r.Value)
|
||||
}
|
||||
|
||||
func TestConfig_Get_Bad(t *testing.T) {
|
||||
c := New()
|
||||
val, ok := c.Config().Get("missing")
|
||||
assert.False(t, ok)
|
||||
assert.Nil(t, val)
|
||||
r := c.Config().Get("missing")
|
||||
assert.False(t, r.OK)
|
||||
assert.Nil(t, r.Value)
|
||||
}
|
||||
|
||||
func TestConfig_TypedAccessors_Good(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ func TestNew_Good(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNew_WithOptions_Good(t *testing.T) {
|
||||
c := New(Options{{K: "name", V: "myapp"}})
|
||||
c := New(Options{{Key: "name", Value: "myapp"}})
|
||||
assert.NotNil(t, c)
|
||||
assert.Equal(t, "myapp", c.App().Name)
|
||||
}
|
||||
|
|
@ -45,9 +45,9 @@ func TestAccessors_Good(t *testing.T) {
|
|||
|
||||
func TestOptions_Accessor_Good(t *testing.T) {
|
||||
c := New(Options{
|
||||
{K: "name", V: "testapp"},
|
||||
{K: "port", V: 8080},
|
||||
{K: "debug", V: true},
|
||||
{Key: "name", Value: "testapp"},
|
||||
{Key: "port", Value: 8080},
|
||||
{Key: "debug", Value: true},
|
||||
})
|
||||
opts := c.Options()
|
||||
assert.NotNil(t, opts)
|
||||
|
|
@ -67,7 +67,7 @@ func TestOptions_Accessor_Nil(t *testing.T) {
|
|||
func TestCore_LogError_Good(t *testing.T) {
|
||||
c := New()
|
||||
cause := assert.AnError
|
||||
r := c.LogError(cause, "test.Op", "something broke")
|
||||
r := c.LogError(cause, "test.Operation", "something broke")
|
||||
assert.False(t, r.OK)
|
||||
err, ok := r.Value.(error)
|
||||
assert.True(t, ok)
|
||||
|
|
@ -76,7 +76,7 @@ func TestCore_LogError_Good(t *testing.T) {
|
|||
|
||||
func TestCore_LogWarn_Good(t *testing.T) {
|
||||
c := New()
|
||||
r := c.LogWarn(assert.AnError, "test.Op", "heads up")
|
||||
r := c.LogWarn(assert.AnError, "test.Operation", "heads up")
|
||||
assert.False(t, r.OK)
|
||||
_, ok := r.Value.(error)
|
||||
assert.True(t, ok)
|
||||
|
|
@ -85,13 +85,13 @@ func TestCore_LogWarn_Good(t *testing.T) {
|
|||
func TestCore_Must_Ugly(t *testing.T) {
|
||||
c := New()
|
||||
assert.Panics(t, func() {
|
||||
c.Must(assert.AnError, "test.Op", "fatal")
|
||||
c.Must(assert.AnError, "test.Operation", "fatal")
|
||||
})
|
||||
}
|
||||
|
||||
func TestCore_Must_Nil_Good(t *testing.T) {
|
||||
c := New()
|
||||
assert.NotPanics(t, func() {
|
||||
c.Must(nil, "test.Op", "no error")
|
||||
c.Must(nil, "test.Operation", "no error")
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@ var testFS embed.FS
|
|||
func TestData_New_Good(t *testing.T) {
|
||||
c := New()
|
||||
r := c.Data().New(Options{
|
||||
{K: "name", V: "test"},
|
||||
{K: "source", V: testFS},
|
||||
{K: "path", V: "testdata"},
|
||||
{Key: "name", Value: "test"},
|
||||
{Key: "source", Value: testFS},
|
||||
{Key: "path", Value: "testdata"},
|
||||
})
|
||||
assert.True(t, r.OK)
|
||||
assert.NotNil(t, r.Value)
|
||||
|
|
@ -28,19 +28,19 @@ func TestData_New_Good(t *testing.T) {
|
|||
func TestData_New_Bad(t *testing.T) {
|
||||
c := New()
|
||||
|
||||
r := c.Data().New(Options{{K: "source", V: testFS}})
|
||||
r := c.Data().New(Options{{Key: "source", Value: testFS}})
|
||||
assert.False(t, r.OK)
|
||||
|
||||
r = c.Data().New(Options{{K: "name", V: "test"}})
|
||||
r = c.Data().New(Options{{Key: "name", Value: "test"}})
|
||||
assert.False(t, r.OK)
|
||||
|
||||
r = c.Data().New(Options{{K: "name", V: "test"}, {K: "source", V: "not-an-fs"}})
|
||||
r = c.Data().New(Options{{Key: "name", Value: "test"}, {Key: "source", Value: "not-an-fs"}})
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
|
||||
func TestData_ReadString_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Data().New(Options{{K: "name", V: "app"}, {K: "source", V: testFS}, {K: "path", V: "testdata"}})
|
||||
c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "testdata"}})
|
||||
r := c.Data().ReadString("app/test.txt")
|
||||
assert.True(t, r.OK)
|
||||
assert.Equal(t, "hello from testdata\n", r.Value.(string))
|
||||
|
|
@ -54,7 +54,7 @@ func TestData_ReadString_Bad(t *testing.T) {
|
|||
|
||||
func TestData_ReadFile_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Data().New(Options{{K: "name", V: "app"}, {K: "source", V: testFS}, {K: "path", V: "testdata"}})
|
||||
c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "testdata"}})
|
||||
r := c.Data().ReadFile("app/test.txt")
|
||||
assert.True(t, r.OK)
|
||||
assert.Equal(t, "hello from testdata\n", string(r.Value.([]byte)))
|
||||
|
|
@ -62,7 +62,7 @@ func TestData_ReadFile_Good(t *testing.T) {
|
|||
|
||||
func TestData_Get_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Data().New(Options{{K: "name", V: "brain"}, {K: "source", V: testFS}, {K: "path", V: "testdata"}})
|
||||
c.Data().New(Options{{Key: "name", Value: "brain"}, {Key: "source", Value: testFS}, {Key: "path", Value: "testdata"}})
|
||||
emb := c.Data().Get("brain")
|
||||
assert.NotNil(t, emb)
|
||||
|
||||
|
|
@ -82,21 +82,21 @@ func TestData_Get_Bad(t *testing.T) {
|
|||
|
||||
func TestData_Mounts_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Data().New(Options{{K: "name", V: "a"}, {K: "source", V: testFS}, {K: "path", V: "testdata"}})
|
||||
c.Data().New(Options{{K: "name", V: "b"}, {K: "source", V: testFS}, {K: "path", V: "testdata"}})
|
||||
c.Data().New(Options{{Key: "name", Value: "a"}, {Key: "source", Value: testFS}, {Key: "path", Value: "testdata"}})
|
||||
c.Data().New(Options{{Key: "name", Value: "b"}, {Key: "source", Value: testFS}, {Key: "path", Value: "testdata"}})
|
||||
mounts := c.Data().Mounts()
|
||||
assert.Len(t, mounts, 2)
|
||||
}
|
||||
|
||||
func TestEmbed_Legacy_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Data().New(Options{{K: "name", V: "app"}, {K: "source", V: testFS}, {K: "path", V: "testdata"}})
|
||||
c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "testdata"}})
|
||||
assert.NotNil(t, c.Embed())
|
||||
}
|
||||
|
||||
func TestData_List_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Data().New(Options{{K: "name", V: "app"}, {K: "source", V: testFS}, {K: "path", V: "."}})
|
||||
c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "."}})
|
||||
r := c.Data().List("app/testdata")
|
||||
assert.True(t, r.OK)
|
||||
}
|
||||
|
|
@ -109,7 +109,7 @@ func TestData_List_Bad(t *testing.T) {
|
|||
|
||||
func TestData_ListNames_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Data().New(Options{{K: "name", V: "app"}, {K: "source", V: testFS}, {K: "path", V: "."}})
|
||||
c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "."}})
|
||||
r := c.Data().ListNames("app/testdata")
|
||||
assert.True(t, r.OK)
|
||||
assert.Contains(t, r.Value.([]string), "test")
|
||||
|
|
@ -117,7 +117,7 @@ func TestData_ListNames_Good(t *testing.T) {
|
|||
|
||||
func TestData_Extract_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Data().New(Options{{K: "name", V: "app"}, {K: "source", V: testFS}, {K: "path", V: "."}})
|
||||
c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "."}})
|
||||
r := c.Data().Extract("app/testdata", t.TempDir(), nil)
|
||||
assert.True(t, r.OK)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ import (
|
|||
func TestDrive_New_Good(t *testing.T) {
|
||||
c := New()
|
||||
r := c.Drive().New(Options{
|
||||
{K: "name", V: "api"},
|
||||
{K: "transport", V: "https://api.lthn.ai"},
|
||||
{Key: "name", Value: "api"},
|
||||
{Key: "transport", Value: "https://api.lthn.ai"},
|
||||
})
|
||||
assert.True(t, r.OK)
|
||||
assert.Equal(t, "api", r.Value.(*DriveHandle).Name)
|
||||
|
|
@ -24,7 +24,7 @@ func TestDrive_New_Bad(t *testing.T) {
|
|||
c := New()
|
||||
// Missing name
|
||||
r := c.Drive().New(Options{
|
||||
{K: "transport", V: "https://api.lthn.ai"},
|
||||
{Key: "transport", Value: "https://api.lthn.ai"},
|
||||
})
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
|
|
@ -32,8 +32,8 @@ func TestDrive_New_Bad(t *testing.T) {
|
|||
func TestDrive_Get_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Drive().New(Options{
|
||||
{K: "name", V: "ssh"},
|
||||
{K: "transport", V: "ssh://claude@10.69.69.165"},
|
||||
{Key: "name", Value: "ssh"},
|
||||
{Key: "transport", Value: "ssh://claude@10.69.69.165"},
|
||||
})
|
||||
handle := c.Drive().Get("ssh")
|
||||
assert.NotNil(t, handle)
|
||||
|
|
@ -48,16 +48,16 @@ func TestDrive_Get_Bad(t *testing.T) {
|
|||
|
||||
func TestDrive_Has_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Drive().New(Options{{K: "name", V: "mcp"}, {K: "transport", V: "mcp://mcp.lthn.sh"}})
|
||||
c.Drive().New(Options{{Key: "name", Value: "mcp"}, {Key: "transport", Value: "mcp://mcp.lthn.sh"}})
|
||||
assert.True(t, c.Drive().Has("mcp"))
|
||||
assert.False(t, c.Drive().Has("missing"))
|
||||
}
|
||||
|
||||
func TestDrive_Names_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Drive().New(Options{{K: "name", V: "api"}, {K: "transport", V: "https://api.lthn.ai"}})
|
||||
c.Drive().New(Options{{K: "name", V: "ssh"}, {K: "transport", V: "ssh://claude@10.69.69.165"}})
|
||||
c.Drive().New(Options{{K: "name", V: "mcp"}, {K: "transport", V: "mcp://mcp.lthn.sh"}})
|
||||
c.Drive().New(Options{{Key: "name", Value: "api"}, {Key: "transport", Value: "https://api.lthn.ai"}})
|
||||
c.Drive().New(Options{{Key: "name", Value: "ssh"}, {Key: "transport", Value: "ssh://claude@10.69.69.165"}})
|
||||
c.Drive().New(Options{{Key: "name", Value: "mcp"}, {Key: "transport", Value: "mcp://mcp.lthn.sh"}})
|
||||
names := c.Drive().Names()
|
||||
assert.Len(t, names, 3)
|
||||
assert.Contains(t, names, "api")
|
||||
|
|
@ -68,9 +68,9 @@ func TestDrive_Names_Good(t *testing.T) {
|
|||
func TestDrive_OptionsPreserved_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Drive().New(Options{
|
||||
{K: "name", V: "api"},
|
||||
{K: "transport", V: "https://api.lthn.ai"},
|
||||
{K: "timeout", V: 30},
|
||||
{Key: "name", Value: "api"},
|
||||
{Key: "transport", Value: "https://api.lthn.ai"},
|
||||
{Key: "timeout", Value: 30},
|
||||
})
|
||||
handle := c.Drive().Get("api")
|
||||
assert.Equal(t, 30, handle.Options.Int("timeout"))
|
||||
|
|
|
|||
|
|
@ -47,9 +47,9 @@ func TestEmbed_Open_Good(t *testing.T) {
|
|||
|
||||
func TestEmbed_ReadDir_Good(t *testing.T) {
|
||||
emb := Mount(testFS, "testdata").Value.(*Embed)
|
||||
entries, err := emb.ReadDir(".")
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, entries)
|
||||
r := emb.ReadDir(".")
|
||||
assert.True(t, r.OK)
|
||||
assert.NotEmpty(t, r.Value)
|
||||
}
|
||||
|
||||
func TestEmbed_Sub_Good(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -104,35 +104,35 @@ func TestFormatStackTrace_Good(t *testing.T) {
|
|||
func TestErrorLog_Good(t *testing.T) {
|
||||
c := New()
|
||||
cause := errors.New("boom")
|
||||
r := c.Log().Error(cause, "test.Op", "something broke")
|
||||
r := c.Log().Error(cause, "test.Operation", "something broke")
|
||||
assert.False(t, r.OK)
|
||||
assert.ErrorIs(t, r.Value.(error), cause)
|
||||
}
|
||||
|
||||
func TestErrorLog_Nil_Good(t *testing.T) {
|
||||
c := New()
|
||||
r := c.Log().Error(nil, "test.Op", "no error")
|
||||
r := c.Log().Error(nil, "test.Operation", "no error")
|
||||
assert.True(t, r.OK)
|
||||
}
|
||||
|
||||
func TestErrorLog_Warn_Good(t *testing.T) {
|
||||
c := New()
|
||||
cause := errors.New("warning")
|
||||
r := c.Log().Warn(cause, "test.Op", "heads up")
|
||||
r := c.Log().Warn(cause, "test.Operation", "heads up")
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
|
||||
func TestErrorLog_Must_Ugly(t *testing.T) {
|
||||
c := New()
|
||||
assert.Panics(t, func() {
|
||||
c.Log().Must(errors.New("fatal"), "test.Op", "must fail")
|
||||
c.Log().Must(errors.New("fatal"), "test.Operation", "must fail")
|
||||
})
|
||||
}
|
||||
|
||||
func TestErrorLog_Must_Nil_Good(t *testing.T) {
|
||||
c := New()
|
||||
assert.NotPanics(t, func() {
|
||||
c.Log().Must(nil, "test.Op", "no error")
|
||||
c.Log().Must(nil, "test.Operation", "no error")
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -179,7 +179,7 @@ func TestAs_Good(t *testing.T) {
|
|||
err := E("op", "msg", nil)
|
||||
var e *Err
|
||||
assert.True(t, As(err, &e))
|
||||
assert.Equal(t, "op", e.Op)
|
||||
assert.Equal(t, "op", e.Operation)
|
||||
}
|
||||
|
||||
func TestNewError_Good(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@ func TestI18n_Good(t *testing.T) {
|
|||
func TestI18n_AddLocales_Good(t *testing.T) {
|
||||
c := New()
|
||||
r := c.Data().New(Options{
|
||||
{K: "name", V: "lang"},
|
||||
{K: "source", V: testFS},
|
||||
{K: "path", V: "testdata"},
|
||||
{Key: "name", Value: "lang"},
|
||||
{Key: "source", Value: testFS},
|
||||
{Key: "path", Value: "testdata"},
|
||||
})
|
||||
if r.OK {
|
||||
c.I18n().AddLocales(r.Value.(*Embed))
|
||||
|
|
@ -38,9 +38,10 @@ func TestI18n_Locales_Empty_Good(t *testing.T) {
|
|||
|
||||
func TestI18n_Translate_NoTranslator_Good(t *testing.T) {
|
||||
c := New()
|
||||
// Without a translator, T returns the key as-is
|
||||
result := c.I18n().Translate("greeting.hello")
|
||||
assert.Equal(t, "greeting.hello", result)
|
||||
// Without a translator, Translate returns the key as-is
|
||||
r := c.I18n().Translate("greeting.hello")
|
||||
assert.True(t, r.OK)
|
||||
assert.Equal(t, "greeting.hello", r.Value)
|
||||
}
|
||||
|
||||
func TestI18n_SetLanguage_NoTranslator_Good(t *testing.T) {
|
||||
|
|
@ -71,10 +72,10 @@ type mockTranslator struct {
|
|||
lang string
|
||||
}
|
||||
|
||||
func (m *mockTranslator) Translate(id string, args ...any) string { return "translated:" + id }
|
||||
func (m *mockTranslator) SetLanguage(lang string) error { m.lang = lang; return nil }
|
||||
func (m *mockTranslator) Language() string { return m.lang }
|
||||
func (m *mockTranslator) AvailableLanguages() []string { return []string{"en", "de", "fr"} }
|
||||
func (m *mockTranslator) Translate(id string, args ...any) Result { return Result{"translated:" + id, true} }
|
||||
func (m *mockTranslator) SetLanguage(lang string) error { m.lang = lang; return nil }
|
||||
func (m *mockTranslator) Language() string { return m.lang }
|
||||
func (m *mockTranslator) AvailableLanguages() []string { return []string{"en", "de", "fr"} }
|
||||
|
||||
func TestI18n_WithTranslator_Good(t *testing.T) {
|
||||
c := New()
|
||||
|
|
@ -82,7 +83,7 @@ func TestI18n_WithTranslator_Good(t *testing.T) {
|
|||
c.I18n().SetTranslator(tr)
|
||||
|
||||
assert.Equal(t, tr, c.I18n().Translator())
|
||||
assert.Equal(t, "translated:hello", c.I18n().Translate("hello"))
|
||||
assert.Equal(t, "translated:hello", c.I18n().Translate("hello").Value)
|
||||
assert.Equal(t, "en", c.I18n().Language())
|
||||
assert.Equal(t, []string{"en", "de", "fr"}, c.I18n().AvailableLanguages())
|
||||
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ func TestLogErr_Good(t *testing.T) {
|
|||
le := NewLogErr(l)
|
||||
assert.NotNil(t, le)
|
||||
|
||||
err := E("test.Op", "something broke", nil)
|
||||
err := E("test.Operation", "something broke", nil)
|
||||
le.Log(err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,34 +11,34 @@ import (
|
|||
|
||||
func TestOptions_Get_Good(t *testing.T) {
|
||||
opts := Options{
|
||||
{K: "name", V: "brain"},
|
||||
{K: "port", V: 8080},
|
||||
{Key: "name", Value: "brain"},
|
||||
{Key: "port", Value: 8080},
|
||||
}
|
||||
val, ok := opts.Get("name")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "brain", val)
|
||||
r := opts.Get("name")
|
||||
assert.True(t, r.OK)
|
||||
assert.Equal(t, "brain", r.Value)
|
||||
}
|
||||
|
||||
func TestOptions_Get_Bad(t *testing.T) {
|
||||
opts := Options{{K: "name", V: "brain"}}
|
||||
val, ok := opts.Get("missing")
|
||||
assert.False(t, ok)
|
||||
assert.Nil(t, val)
|
||||
opts := Options{{Key: "name", Value: "brain"}}
|
||||
r := opts.Get("missing")
|
||||
assert.False(t, r.OK)
|
||||
assert.Nil(t, r.Value)
|
||||
}
|
||||
|
||||
func TestOptions_Has_Good(t *testing.T) {
|
||||
opts := Options{{K: "debug", V: true}}
|
||||
opts := Options{{Key: "debug", Value: true}}
|
||||
assert.True(t, opts.Has("debug"))
|
||||
assert.False(t, opts.Has("missing"))
|
||||
}
|
||||
|
||||
func TestOptions_String_Good(t *testing.T) {
|
||||
opts := Options{{K: "name", V: "brain"}}
|
||||
opts := Options{{Key: "name", Value: "brain"}}
|
||||
assert.Equal(t, "brain", opts.String("name"))
|
||||
}
|
||||
|
||||
func TestOptions_String_Bad(t *testing.T) {
|
||||
opts := Options{{K: "port", V: 8080}}
|
||||
opts := Options{{Key: "port", Value: 8080}}
|
||||
// Wrong type — returns empty string
|
||||
assert.Equal(t, "", opts.String("port"))
|
||||
// Missing key — returns empty string
|
||||
|
|
@ -46,40 +46,40 @@ func TestOptions_String_Bad(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestOptions_Int_Good(t *testing.T) {
|
||||
opts := Options{{K: "port", V: 8080}}
|
||||
opts := Options{{Key: "port", Value: 8080}}
|
||||
assert.Equal(t, 8080, opts.Int("port"))
|
||||
}
|
||||
|
||||
func TestOptions_Int_Bad(t *testing.T) {
|
||||
opts := Options{{K: "name", V: "brain"}}
|
||||
opts := Options{{Key: "name", Value: "brain"}}
|
||||
assert.Equal(t, 0, opts.Int("name"))
|
||||
assert.Equal(t, 0, opts.Int("missing"))
|
||||
}
|
||||
|
||||
func TestOptions_Bool_Good(t *testing.T) {
|
||||
opts := Options{{K: "debug", V: true}}
|
||||
opts := Options{{Key: "debug", Value: true}}
|
||||
assert.True(t, opts.Bool("debug"))
|
||||
}
|
||||
|
||||
func TestOptions_Bool_Bad(t *testing.T) {
|
||||
opts := Options{{K: "name", V: "brain"}}
|
||||
opts := Options{{Key: "name", Value: "brain"}}
|
||||
assert.False(t, opts.Bool("name"))
|
||||
assert.False(t, opts.Bool("missing"))
|
||||
}
|
||||
|
||||
func TestOptions_TypedStruct_Good(t *testing.T) {
|
||||
// Packages plug typed structs into Option.V
|
||||
// Packages plug typed structs into Option.Value
|
||||
type BrainConfig struct {
|
||||
Name string
|
||||
OllamaURL string
|
||||
Collection string
|
||||
}
|
||||
cfg := BrainConfig{Name: "brain", OllamaURL: "http://localhost:11434", Collection: "openbrain"}
|
||||
opts := Options{{K: "config", V: cfg}}
|
||||
opts := Options{{Key: "config", Value: cfg}}
|
||||
|
||||
val, ok := opts.Get("config")
|
||||
assert.True(t, ok)
|
||||
bc, ok := val.(BrainConfig)
|
||||
r := opts.Get("config")
|
||||
assert.True(t, r.OK)
|
||||
bc, ok := r.Value.(BrainConfig)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "brain", bc.Name)
|
||||
assert.Equal(t, "http://localhost:11434", bc.OllamaURL)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue