From a49bc46bc72a5ee98fe6a935143f063861fc4f7c Mon Sep 17 00:00:00 2001 From: Snider Date: Tue, 24 Mar 2026 19:17:12 +0000 Subject: [PATCH] feat: Options struct + Result methods + WithOption convenience Options is now a proper struct with New(), Set(), Get(), typed accessors. Result gains New(), Result(), Get() methods on the struct. WithOption("key", value) convenience for core.New(). options_test.go: 22 tests passing against the new contract. Other test files mechanically updated for compilation. Co-Authored-By: Virgil --- app_test.go | 2 +- cli.go | 8 +-- cli_test.go | 14 +++--- command_test.go | 28 +++++------ contract.go | 28 +++++++++-- core_test.go | 6 +-- data_test.go | 24 ++++----- drive.go | 4 +- drive_test.go | 8 +-- embed.go | 2 +- fs.go | 10 ++-- lock_test.go | 4 +- options.go | 123 +++++++++++++++++++++++++++++---------------- options_test.go | 129 +++++++++++++++++++++++++++++++++++++++--------- runtime.go | 2 +- service_test.go | 2 +- 16 files changed, 267 insertions(+), 127 deletions(-) diff --git a/app_test.go b/app_test.go index 406cdb4..2460fab 100644 --- a/app_test.go +++ b/app_test.go @@ -10,7 +10,7 @@ import ( // --- App --- func TestApp_Good(t *testing.T) { - c := New(WithOptions(Options{{Key: "name", Value: "myapp"}})).Value.(*Core) + c := New(WithOptions(NewOptions(Option{Key: "name", Value: "myapp"}))).Value.(*Core) assert.Equal(t, "myapp", c.App().Name) } diff --git a/cli.go b/cli.go index ff7d298..4428974 100644 --- a/cli.go +++ b/cli.go @@ -92,17 +92,17 @@ func (cl *Cli) Run(args ...string) Result { } // Build options from remaining args - opts := Options{} + opts := NewOptions() for _, arg := range remaining { key, val, valid := ParseFlag(arg) if valid { if Contains(arg, "=") { - opts = append(opts, Option{Key: key, Value: val}) + opts.Set(key, val) } else { - opts = append(opts, Option{Key: key, Value: true}) + opts.Set(key, true) } } else if !IsFlag(arg) { - opts = append(opts, Option{Key: "_arg", Value: arg}) + opts.Set("_arg", arg) } } diff --git a/cli_test.go b/cli_test.go index f29a467..8bc9de2 100644 --- a/cli_test.go +++ b/cli_test.go @@ -16,7 +16,7 @@ func TestCli_Good(t *testing.T) { } func TestCli_Banner_Good(t *testing.T) { - c := New(WithOptions(Options{{Key: "name", Value: "myapp"}})).Value.(*Core) + c := New(WithOptions(NewOptions(Option{Key: "name", Value: "myapp"}))).Value.(*Core) assert.Equal(t, "myapp", c.Cli().Banner()) } @@ -32,7 +32,7 @@ func TestCli_Run_Good(t *testing.T) { c.Command("hello", Command{Action: func(_ Options) Result { executed = true return Result{Value: "world", OK: true} - }}) + })) r := c.Cli().Run("hello") assert.True(t, r.OK) assert.Equal(t, "world", r.Value) @@ -45,7 +45,7 @@ func TestCli_Run_Nested_Good(t *testing.T) { c.Command("deploy/to/homelab", Command{Action: func(_ Options) Result { executed = true return Result{OK: true} - }}) + })) r := c.Cli().Run("deploy", "to", "homelab") assert.True(t, r.OK) assert.True(t, executed) @@ -57,7 +57,7 @@ func TestCli_Run_WithFlags_Good(t *testing.T) { c.Command("serve", Command{Action: func(opts Options) Result { received = opts return Result{OK: true} - }}) + })) c.Cli().Run("serve", "--port=8080", "--debug") assert.Equal(t, "8080", received.String("port")) assert.True(t, received.Bool("debug")) @@ -70,9 +70,9 @@ func TestCli_Run_NoCommand_Good(t *testing.T) { } func TestCli_PrintHelp_Good(t *testing.T) { - c := New(WithOptions(Options{{Key: "name", Value: "myapp"}})).Value.(*Core) - 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 := New(WithOptions(NewOptions(Option{Key: "name", Value: "myapp"}))).Value.(*Core) + 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() } diff --git a/command_test.go b/command_test.go index 3e0bb91..b3cd62d 100644 --- a/command_test.go +++ b/command_test.go @@ -13,13 +13,13 @@ func TestCommand_Register_Good(t *testing.T) { c := New().Value.(*Core) r := c.Command("deploy", Command{Action: func(_ Options) Result { return Result{Value: "deployed", OK: true} - }}) + })) assert.True(t, r.OK) } func TestCommand_Get_Good(t *testing.T) { c := New().Value.(*Core) - c.Command("deploy", Command{Action: func(_ Options) Result { return Result{OK: true} }}) + c.Command("deploy", Command{Action: func(_ Options) Result { return Result{OK: true} })) r := c.Command("deploy") assert.True(t, r.OK) assert.NotNil(t, r.Value) @@ -35,9 +35,9 @@ func TestCommand_Run_Good(t *testing.T) { c := New().Value.(*Core) c.Command("greet", Command{Action: func(opts Options) Result { return Result{Value: Concat("hello ", opts.String("name")), OK: true} - }}) + })) cmd := c.Command("greet").Value.(*Command) - r := cmd.Run(Options{{Key: "name", Value: "world"}}) + r := cmd.Run(NewOptions(Option{Key: "name", Value: "world"})) assert.True(t, r.OK) assert.Equal(t, "hello world", r.Value) } @@ -46,7 +46,7 @@ func TestCommand_Run_NoAction_Good(t *testing.T) { c := New().Value.(*Core) c.Command("empty", Command{Description: "no action"}) cmd := c.Command("empty").Value.(*Command) - r := cmd.Run(Options{}) + r := cmd.Run(NewOptions()) assert.False(t, r.OK) } @@ -56,7 +56,7 @@ func TestCommand_Nested_Good(t *testing.T) { c := New().Value.(*Core) c.Command("deploy/to/homelab", Command{Action: func(_ Options) Result { return Result{Value: "deployed to homelab", OK: true} - }}) + })) r := c.Command("deploy/to/homelab") assert.True(t, r.OK) @@ -68,9 +68,9 @@ func TestCommand_Nested_Good(t *testing.T) { func TestCommand_Paths_Good(t *testing.T) { c := New().Value.(*Core) - 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.Command("deploy/to/homelab", Command{Action: func(_ Options) Result { return Result{OK: true} }}) + 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.Command("deploy/to/homelab", Command{Action: func(_ Options) Result { return Result{OK: true} })) paths := c.Commands() assert.Contains(t, paths, "deploy") @@ -108,10 +108,10 @@ func TestCommand_Lifecycle_NoImpl_Good(t *testing.T) { c := New().Value.(*Core) c.Command("serve", Command{Action: func(_ Options) Result { return Result{Value: "running", OK: true} - }}) + })) cmd := c.Command("serve").Value.(*Command) - r := cmd.Start(Options{}) + r := cmd.Start(NewOptions()) assert.True(t, r.OK) assert.Equal(t, "running", r.Value) @@ -158,7 +158,7 @@ func TestCommand_Lifecycle_WithImpl_Good(t *testing.T) { c.Command("daemon", Command{Lifecycle: lc}) cmd := c.Command("daemon").Value.(*Command) - r := cmd.Start(Options{}) + r := cmd.Start(NewOptions()) assert.True(t, r.OK) assert.True(t, lc.started) @@ -178,8 +178,8 @@ func TestCommand_Lifecycle_WithImpl_Good(t *testing.T) { func TestCommand_Duplicate_Bad(t *testing.T) { c := New().Value.(*Core) - c.Command("deploy", Command{Action: func(_ Options) Result { return Result{OK: true} }}) - r := c.Command("deploy", Command{Action: func(_ Options) Result { return Result{OK: true} }}) + c.Command("deploy", Command{Action: func(_ Options) Result { return Result{OK: true} })) + r := c.Command("deploy", Command{Action: func(_ Options) Result { return Result{OK: true} })) assert.False(t, r.OK) } diff --git a/contract.go b/contract.go index 595c0fb..43b82c0 100644 --- a/contract.go +++ b/contract.go @@ -120,10 +120,8 @@ func New(opts ...CoreOption) Result { // core.WithOptions(core.Options{{Key: "name", Value: "myapp"}}) func WithOptions(opts Options) CoreOption { return func(c *Core) Result { - cp := make(Options, len(opts)) - copy(cp, opts) - c.options = &cp - if name := cp.String("name"); name != "" { + c.options = &opts + if name := opts.String("name"); name != "" { c.app.Name = name } return Result{OK: true} @@ -142,6 +140,28 @@ func WithService(factory func(*Core) Result) CoreOption { } } +// WithOption is a convenience for setting a single key-value option. +// +// core.New( +// core.WithOption("name", "myapp"), +// core.WithOption("port", 8080), +// ) +func WithOption(key string, value any) CoreOption { + return func(c *Core) Result { + if c.options == nil { + opts := NewOptions() + c.options = &opts + } + c.options.Set(key, value) + if key == "name" { + if s, ok := value.(string); ok { + c.app.Name = s + } + } + return Result{OK: true} + } +} + // WithServiceLock prevents further service registration after construction. // // core.New( diff --git a/core_test.go b/core_test.go index 6c2d3f7..601c37b 100644 --- a/core_test.go +++ b/core_test.go @@ -16,21 +16,21 @@ func TestNew_Good(t *testing.T) { } func TestNew_WithOptions_Good(t *testing.T) { - c := New(WithOptions(Options{{Key: "name", Value: "myapp"}})).Value.(*Core) + c := New(WithOptions(NewOptions(Option{Key: "name", Value: "myapp"}))).Value.(*Core) assert.NotNil(t, c) assert.Equal(t, "myapp", c.App().Name) } func TestNew_WithOptions_Bad(t *testing.T) { // Empty options — should still create a valid Core - c := New(WithOptions(Options{})).Value.(*Core) + c := New(WithOptions(NewOptions())).Value.(*Core) assert.NotNil(t, c) } func TestNew_WithService_Good(t *testing.T) { started := false r := New( - WithOptions(Options{{Key: "name", Value: "myapp"}}), + WithOptions(NewOptions(Option{Key: "name", Value: "myapp"})), WithService(func(c *Core) Result { c.Service("test", Service{ OnStart: func() Result { started = true; return Result{OK: true} }, diff --git a/data_test.go b/data_test.go index 81ade81..61972d9 100644 --- a/data_test.go +++ b/data_test.go @@ -28,19 +28,19 @@ func TestData_New_Good(t *testing.T) { func TestData_New_Bad(t *testing.T) { c := New().Value.(*Core) - r := c.Data().New(Options{{Key: "source", Value: testFS}}) + r := c.Data().New(NewOptions(Option{Key: "source", Value: testFS})) assert.False(t, r.OK) - r = c.Data().New(Options{{Key: "name", Value: "test"}}) + r = c.Data().New(NewOptions(Option{Key: "name", Value: "test"})) assert.False(t, r.OK) - r = c.Data().New(Options{{Key: "name", Value: "test"}, {Key: "source", Value: "not-an-fs"}}) + r = c.Data().New(NewOptions(Option{Key: "name", Value: "test"}, Option{Key: "source", Value: "not-an-fs"})) assert.False(t, r.OK) } func TestData_ReadString_Good(t *testing.T) { c := New().Value.(*Core) - c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "testdata"}}) + c.Data().New(NewOptions(Option{Key: "name", Value: "app"}, Option{Key: "source", Value: testFS}, Option{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().Value.(*Core) - c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "testdata"}}) + c.Data().New(NewOptions(Option{Key: "name", Value: "app"}, Option{Key: "source", Value: testFS}, Option{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().Value.(*Core) - c.Data().New(Options{{Key: "name", Value: "brain"}, {Key: "source", Value: testFS}, {Key: "path", Value: "testdata"}}) + c.Data().New(NewOptions(Option{Key: "name", Value: "brain"}, Option{Key: "source", Value: testFS}, Option{Key: "path", Value: "testdata"})) gr := c.Data().Get("brain") assert.True(t, gr.OK) emb := gr.Value.(*Embed) @@ -83,21 +83,21 @@ func TestData_Get_Bad(t *testing.T) { func TestData_Mounts_Good(t *testing.T) { c := New().Value.(*Core) - 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"}}) + c.Data().New(NewOptions(Option{Key: "name", Value: "a"}, Option{Key: "source", Value: testFS}, Option{Key: "path", Value: "testdata"})) + c.Data().New(NewOptions(Option{Key: "name", Value: "b"}, Option{Key: "source", Value: testFS}, Option{Key: "path", Value: "testdata"})) mounts := c.Data().Mounts() assert.Len(t, mounts, 2) } func TestEmbed_Legacy_Good(t *testing.T) { c := New().Value.(*Core) - c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "testdata"}}) + c.Data().New(NewOptions(Option{Key: "name", Value: "app"}, Option{Key: "source", Value: testFS}, Option{Key: "path", Value: "testdata"})) assert.NotNil(t, c.Embed()) } func TestData_List_Good(t *testing.T) { c := New().Value.(*Core) - c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "."}}) + c.Data().New(NewOptions(Option{Key: "name", Value: "app"}, Option{Key: "source", Value: testFS}, Option{Key: "path", Value: "."})) r := c.Data().List("app/testdata") assert.True(t, r.OK) } @@ -110,7 +110,7 @@ func TestData_List_Bad(t *testing.T) { func TestData_ListNames_Good(t *testing.T) { c := New().Value.(*Core) - c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "."}}) + c.Data().New(NewOptions(Option{Key: "name", Value: "app"}, Option{Key: "source", Value: testFS}, Option{Key: "path", Value: "."})) r := c.Data().ListNames("app/testdata") assert.True(t, r.OK) assert.Contains(t, r.Value.([]string), "test") @@ -118,7 +118,7 @@ func TestData_ListNames_Good(t *testing.T) { func TestData_Extract_Good(t *testing.T) { c := New().Value.(*Core) - c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "."}}) + c.Data().New(NewOptions(Option{Key: "name", Value: "app"}, Option{Key: "source", Value: testFS}, Option{Key: "path", Value: "."})) r := c.Data().Extract("app/testdata", t.TempDir(), nil) assert.True(t, r.OK) } diff --git a/drive.go b/drive.go index e6988c4..cbe9ac6 100644 --- a/drive.go +++ b/drive.go @@ -62,12 +62,10 @@ func (d *Drive) New(opts Options) Result { d.handles = make(map[string]*DriveHandle) } - cp := make(Options, len(opts)) - copy(cp, opts) handle := &DriveHandle{ Name: name, Transport: transport, - Options: cp, + Options: opts, } d.handles[name] = handle diff --git a/drive_test.go b/drive_test.go index afd6a34..caa8440 100644 --- a/drive_test.go +++ b/drive_test.go @@ -49,16 +49,16 @@ func TestDrive_Get_Bad(t *testing.T) { func TestDrive_Has_Good(t *testing.T) { c := New().Value.(*Core) - c.Drive().New(Options{{Key: "name", Value: "mcp"}, {Key: "transport", Value: "mcp://mcp.lthn.sh"}}) + c.Drive().New(NewOptions(Option{Key: "name", Value: "mcp"}, Option{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().Value.(*Core) - 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"}}) + c.Drive().New(NewOptions(Option{Key: "name", Value: "api"}, Option{Key: "transport", Value: "https://api.lthn.ai"})) + c.Drive().New(NewOptions(Option{Key: "name", Value: "ssh"}, Option{Key: "transport", Value: "ssh://claude@10.69.69.165"})) + c.Drive().New(NewOptions(Option{Key: "name", Value: "mcp"}, Option{Key: "transport", Value: "mcp://mcp.lthn.sh"})) names := c.Drive().Names() assert.Len(t, names, 3) assert.Contains(t, names, "api") diff --git a/embed.go b/embed.go index e6a5766..7951543 100644 --- a/embed.go +++ b/embed.go @@ -396,7 +396,7 @@ func (s *Embed) ReadDir(name string) Result { if !r.OK { return r } - return Result{}.Result(fs.ReadDir(s.fsys, r.Value.(string))) + return Result{}.New(fs.ReadDir(s.fsys, r.Value.(string))) } // ReadFile reads the named file. diff --git a/fs.go b/fs.go index 249ddaf..5a28d21 100644 --- a/fs.go +++ b/fs.go @@ -190,7 +190,7 @@ func (m *Fs) List(p string) Result { if !vp.OK { return vp } - return Result{}.Result(os.ReadDir(vp.Value.(string))) + return Result{}.New(os.ReadDir(vp.Value.(string))) } // Stat returns file info. @@ -199,7 +199,7 @@ func (m *Fs) Stat(p string) Result { if !vp.OK { return vp } - return Result{}.Result(os.Stat(vp.Value.(string))) + return Result{}.New(os.Stat(vp.Value.(string))) } // Open opens the named file for reading. @@ -208,7 +208,7 @@ func (m *Fs) Open(p string) Result { if !vp.OK { return vp } - return Result{}.Result(os.Open(vp.Value.(string))) + return Result{}.New(os.Open(vp.Value.(string))) } // Create creates or truncates the named file. @@ -221,7 +221,7 @@ func (m *Fs) Create(p string) Result { if err := os.MkdirAll(filepath.Dir(full), 0755); err != nil { return Result{err, false} } - return Result{}.Result(os.Create(full)) + return Result{}.New(os.Create(full)) } // Append opens the named file for appending, creating it if it doesn't exist. @@ -234,7 +234,7 @@ func (m *Fs) Append(p string) Result { if err := os.MkdirAll(filepath.Dir(full), 0755); err != nil { return Result{err, false} } - return Result{}.Result(os.OpenFile(full, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)) + return Result{}.New(os.OpenFile(full, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)) } // ReadStream returns a reader for the file content. diff --git a/lock_test.go b/lock_test.go index 93b574a..7f19123 100644 --- a/lock_test.go +++ b/lock_test.go @@ -40,7 +40,7 @@ func TestLockEnable_Good(t *testing.T) { func TestStartables_Good(t *testing.T) { c := New().Value.(*Core) - c.Service("s", Service{OnStart: func() Result { return Result{OK: true} }}) + c.Service("s", Service{OnStart: func() Result { return Result{OK: true} })) r := c.Startables() assert.True(t, r.OK) assert.Len(t, r.Value.([]*Service), 1) @@ -48,7 +48,7 @@ func TestStartables_Good(t *testing.T) { func TestStoppables_Good(t *testing.T) { c := New().Value.(*Core) - c.Service("s", Service{OnStop: func() Result { return Result{OK: true} }}) + c.Service("s", Service{OnStop: func() Result { return Result{OK: true} })) r := c.Stoppables() assert.True(t, r.OK) assert.Len(t, r.Value.([]*Service), 1) diff --git a/options.go b/options.go index 4d4c5f8..443515a 100644 --- a/options.go +++ b/options.go @@ -2,42 +2,24 @@ // Core primitives: Option, Options, Result. // -// Option is a single key-value pair. Options is a collection. -// Any function that returns Result can accept Options. +// Options is the universal input type. Result is the universal output type. +// All Core operations accept Options and return Result. // -// Create options: -// -// opts := core.Options{ -// {Key: "name", Value: "brain"}, -// {Key: "path", Value: "prompts"}, -// } -// -// Read options: -// -// name := opts.String("name") -// port := opts.Int("port") -// ok := opts.Has("debug") -// -// Use with subsystems: -// -// c.Drive().New(core.Options{ -// {Key: "name", Value: "brain"}, -// {Key: "source", Value: brainFS}, -// {Key: "path", Value: "prompts"}, -// }) -// -// Use with New: -// -// c := core.New(core.Options{ -// {Key: "name", Value: "myapp"}, -// }) +// opts := core.NewOptions( +// core.Option{Key: "name", Value: "brain"}, +// core.Option{Key: "path", Value: "prompts"}, +// ) +// r := c.Drive().New(opts) +// if !r.OK { log.Fatal(r.Error()) } package core +// --- Result: Universal Output --- + // 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{{Key: "name", Value: "brain"}}) -// if r.OK { use(r.Result()) } +// r := c.Data().New(opts) +// if !r.OK { core.Error("failed", "err", r.Error()) } type Result struct { Value any OK bool @@ -50,21 +32,34 @@ type Result struct { // r.Result(value) // OK = true, Value = value // r.Result() // after set — returns the value func (r Result) Result(args ...any) Result { - if len(args) == 0 { + if args == nil { return r } + return r.New(args...) +} - if len(args) == 1 { - return Result{args[0], true} +func (r Result) New(args ...any) Result { + if len(args) >= 1 { + r.Value = args[0] } - if err, ok := args[len(args)-1].(error); ok { + if err, ok := r.Value.(error); ok { if err != nil { - return Result{err, false} + r.Value = err + r.OK = false + } else { + r.OK = true } - return Result{args[0], true} } - return Result{args[0], true} + + return r +} + +func (r Result) Get() Result { + if r.OK { + return r + } + return Result{Value: r.Value, OK: false} } // Option is a single key-value configuration pair. @@ -76,19 +71,51 @@ type Option struct { Value any } -// Options is a collection of Option items. -// The universal input type for Core operations. +// --- Options: Universal Input --- + +// Options is the universal input type for Core operations. +// A structured collection of key-value pairs with typed accessors. // -// opts := core.Options{{Key: "name", Value: "myapp"}} +// opts := core.NewOptions( +// core.Option{Key: "name", Value: "myapp"}, +// core.Option{Key: "port", Value: 8080}, +// ) // name := opts.String("name") -type Options []Option +type Options struct { + items []Option +} + +// NewOptions creates an Options collection from key-value pairs. +// +// opts := core.NewOptions( +// core.Option{Key: "name", Value: "brain"}, +// core.Option{Key: "path", Value: "prompts"}, +// ) +func NewOptions(items ...Option) Options { + cp := make([]Option, len(items)) + copy(cp, items) + return Options{items: cp} +} + +// Set adds or updates a key-value pair. +// +// opts.Set("port", 8080) +func (o *Options) Set(key string, value any) { + for i, opt := range o.items { + if opt.Key == key { + o.items[i].Value = value + return + } + } + o.items = append(o.items, Option{Key: key, Value: value}) +} // Get retrieves a value by key. // // r := opts.Get("name") // if r.OK { name := r.Value.(string) } func (o Options) Get(key string) Result { - for _, opt := range o { + for _, opt := range o.items { if opt.Key == key { return Result{opt.Value, true} } @@ -138,3 +165,15 @@ func (o Options) Bool(key string) bool { b, _ := r.Value.(bool) return b } + +// Len returns the number of options. +func (o Options) Len() int { + return len(o.items) +} + +// Items returns a copy of the underlying option slice. +func (o Options) Items() []Option { + cp := make([]Option, len(o.items)) + copy(cp, o.items) + return cp +} diff --git a/options_test.go b/options_test.go index 4556062..c839a3d 100644 --- a/options_test.go +++ b/options_test.go @@ -7,75 +7,121 @@ import ( "github.com/stretchr/testify/assert" ) -// --- Option / Options --- +// --- NewOptions --- + +func TestNewOptions_Good(t *testing.T) { + opts := NewOptions( + Option{Key: "name", Value: "brain"}, + Option{Key: "port", Value: 8080}, + ) + assert.Equal(t, 2, opts.Len()) +} + +func TestNewOptions_Empty_Good(t *testing.T) { + opts := NewOptions() + assert.Equal(t, 0, opts.Len()) + assert.False(t, opts.Has("anything")) +} + +// --- Options.Set --- + +func TestOptions_Set_Good(t *testing.T) { + opts := NewOptions() + opts.Set("name", "brain") + assert.Equal(t, "brain", opts.String("name")) +} + +func TestOptions_Set_Update_Good(t *testing.T) { + opts := NewOptions(Option{Key: "name", Value: "old"}) + opts.Set("name", "new") + assert.Equal(t, "new", opts.String("name")) + assert.Equal(t, 1, opts.Len()) +} + +// --- Options.Get --- func TestOptions_Get_Good(t *testing.T) { - opts := Options{ - {Key: "name", Value: "brain"}, - {Key: "port", Value: 8080}, - } + opts := NewOptions( + Option{Key: "name", Value: "brain"}, + Option{Key: "port", Value: 8080}, + ) r := opts.Get("name") assert.True(t, r.OK) assert.Equal(t, "brain", r.Value) } func TestOptions_Get_Bad(t *testing.T) { - opts := Options{{Key: "name", Value: "brain"}} + opts := NewOptions(Option{Key: "name", Value: "brain"}) r := opts.Get("missing") assert.False(t, r.OK) assert.Nil(t, r.Value) } +// --- Options.Has --- + func TestOptions_Has_Good(t *testing.T) { - opts := Options{{Key: "debug", Value: true}} + opts := NewOptions(Option{Key: "debug", Value: true}) assert.True(t, opts.Has("debug")) assert.False(t, opts.Has("missing")) } +// --- Options.String --- + func TestOptions_String_Good(t *testing.T) { - opts := Options{{Key: "name", Value: "brain"}} + opts := NewOptions(Option{Key: "name", Value: "brain"}) assert.Equal(t, "brain", opts.String("name")) } func TestOptions_String_Bad(t *testing.T) { - opts := Options{{Key: "port", Value: 8080}} - // Wrong type — returns empty string + opts := NewOptions(Option{Key: "port", Value: 8080}) assert.Equal(t, "", opts.String("port")) - // Missing key — returns empty string assert.Equal(t, "", opts.String("missing")) } +// --- Options.Int --- + func TestOptions_Int_Good(t *testing.T) { - opts := Options{{Key: "port", Value: 8080}} + opts := NewOptions(Option{Key: "port", Value: 8080}) assert.Equal(t, 8080, opts.Int("port")) } func TestOptions_Int_Bad(t *testing.T) { - opts := Options{{Key: "name", Value: "brain"}} + opts := NewOptions(Option{Key: "name", Value: "brain"}) assert.Equal(t, 0, opts.Int("name")) assert.Equal(t, 0, opts.Int("missing")) } +// --- Options.Bool --- + func TestOptions_Bool_Good(t *testing.T) { - opts := Options{{Key: "debug", Value: true}} + opts := NewOptions(Option{Key: "debug", Value: true}) assert.True(t, opts.Bool("debug")) } func TestOptions_Bool_Bad(t *testing.T) { - opts := Options{{Key: "name", Value: "brain"}} + opts := NewOptions(Option{Key: "name", Value: "brain"}) assert.False(t, opts.Bool("name")) assert.False(t, opts.Bool("missing")) } +// --- Options.Items --- + +func TestOptions_Items_Good(t *testing.T) { + opts := NewOptions(Option{Key: "a", Value: 1}, Option{Key: "b", Value: 2}) + items := opts.Items() + assert.Len(t, items, 2) +} + +// --- Options with typed struct --- + func TestOptions_TypedStruct_Good(t *testing.T) { - // 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{{Key: "config", Value: cfg}} + opts := NewOptions(Option{Key: "config", Value: cfg}) r := opts.Get("config") assert.True(t, r.OK) @@ -85,10 +131,47 @@ func TestOptions_TypedStruct_Good(t *testing.T) { assert.Equal(t, "http://localhost:11434", bc.OllamaURL) } -func TestOptions_Empty_Good(t *testing.T) { - opts := Options{} - assert.False(t, opts.Has("anything")) - assert.Equal(t, "", opts.String("anything")) - assert.Equal(t, 0, opts.Int("anything")) - assert.False(t, opts.Bool("anything")) +// --- Result --- + +func TestResult_New_Good(t *testing.T) { + r := Result{}.New("value") + assert.Equal(t, "value", r.Value) +} + +func TestResult_New_Error_Bad(t *testing.T) { + err := E("test", "failed", nil) + r := Result{}.New(err) + assert.False(t, r.OK) + assert.Equal(t, err, r.Value) +} + +func TestResult_Result_Good(t *testing.T) { + r := Result{Value: "hello", OK: true} + assert.Equal(t, r, r.Result()) +} + +func TestResult_Result_WithArgs_Good(t *testing.T) { + r := Result{}.Result("value") + assert.Equal(t, "value", r.Value) +} + +func TestResult_Get_Good(t *testing.T) { + r := Result{Value: "hello", OK: true} + assert.True(t, r.Get().OK) +} + +func TestResult_Get_Bad(t *testing.T) { + r := Result{Value: "err", OK: false} + assert.False(t, r.Get().OK) +} + +// --- WithOption --- + +func TestWithOption_Good(t *testing.T) { + c := New( + WithOption("name", "myapp"), + WithOption("port", 8080), + ).Value.(*Core) + assert.Equal(t, "myapp", c.App().Name) + assert.Equal(t, 8080, c.Options().Int("port")) } diff --git a/runtime.go b/runtime.go index 3e48afb..f98840f 100644 --- a/runtime.go +++ b/runtime.go @@ -106,7 +106,7 @@ type ServiceFactory func() Result // NewWithFactories creates a Runtime with the provided service factories. func NewWithFactories(app any, factories map[string]ServiceFactory) Result { - r := New(WithOptions(Options{{Key: "name", Value: "core"}})) + r := New(WithOptions(NewOptions(Option{Key: "name", Value: "core"}))) if !r.OK { return r } diff --git a/service_test.go b/service_test.go index ddd32fd..8dd3123 100644 --- a/service_test.go +++ b/service_test.go @@ -30,7 +30,7 @@ func TestService_Register_Empty_Bad(t *testing.T) { func TestService_Get_Good(t *testing.T) { c := New().Value.(*Core) - c.Service("brain", Service{OnStart: func() Result { return Result{OK: true} }}) + c.Service("brain", Service{OnStart: func() Result { return Result{OK: true} })) r := c.Service("brain") assert.True(t, r.OK) assert.NotNil(t, r.Value)