feat: App struct with New(Options) + Find() as method

App.New() creates from Options. App.Find() locates programs on PATH.
Both are struct methods — no package-level functions.
8 tests passing.

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-24 19:18:59 +00:00
parent d5e388fbb2
commit d8fb18a663
2 changed files with 68 additions and 25 deletions

52
app.go
View file

@ -1,7 +1,6 @@
// SPDX-License-Identifier: EUPL-1.2
// Application identity for the Core framework.
// Based on leaanthony/sail — Name, Filename, Path.
package core
@ -11,32 +10,47 @@ import (
)
// App holds the application identity and optional GUI runtime.
//
// app := core.App{}.New(core.NewOptions(
// core.Option{Key: "name", Value: "Core CLI"},
// core.Option{Key: "version", Value: "1.0.0"},
// ))
type App struct {
// Name is the human-readable application name (e.g., "Core CLI").
Name string
// Version is the application version string (e.g., "1.2.3").
Version string
// Description is a short description of the application.
Name string
Version string
Description string
Filename string
Path string
Runtime any // GUI runtime (e.g., Wails App). Nil for CLI-only.
}
// Filename is the executable filename (e.g., "core").
Filename string
// Path is the absolute path to the executable.
Path string
// Runtime is the GUI runtime (e.g., Wails App).
// Nil for CLI-only applications.
Runtime any
// New creates an App from Options.
//
// app := core.App{}.New(core.NewOptions(
// core.Option{Key: "name", Value: "myapp"},
// core.Option{Key: "version", Value: "1.0.0"},
// ))
func (a App) New(opts Options) App {
if name := opts.String("name"); name != "" {
a.Name = name
}
if version := opts.String("version"); version != "" {
a.Version = version
}
if desc := opts.String("description"); desc != "" {
a.Description = desc
}
if filename := opts.String("filename"); filename != "" {
a.Filename = filename
}
return a
}
// Find locates a program on PATH and returns a Result containing the App.
//
// r := core.Find("node", "Node.js")
// r := core.App{}.Find("node", "Node.js")
// if r.OK { app := r.Value.(*App) }
func Find(filename, name string) Result {
func (a App) Find(filename, name string) Result {
path, err := exec.LookPath(filename)
if err != nil {
return Result{err, false}

View file

@ -7,14 +7,41 @@ import (
"github.com/stretchr/testify/assert"
)
// --- App ---
// --- App.New ---
func TestApp_Good(t *testing.T) {
c := New(WithOptions(NewOptions(Option{Key: "name", Value: "myapp"}))).Value.(*Core)
func TestApp_New_Good(t *testing.T) {
app := App{}.New(NewOptions(
Option{Key: "name", Value: "myapp"},
Option{Key: "version", Value: "1.0.0"},
Option{Key: "description", Value: "test app"},
))
assert.Equal(t, "myapp", app.Name)
assert.Equal(t, "1.0.0", app.Version)
assert.Equal(t, "test app", app.Description)
}
func TestApp_New_Empty_Good(t *testing.T) {
app := App{}.New(NewOptions())
assert.Equal(t, "", app.Name)
assert.Equal(t, "", app.Version)
}
func TestApp_New_Partial_Good(t *testing.T) {
app := App{}.New(NewOptions(
Option{Key: "name", Value: "myapp"},
))
assert.Equal(t, "myapp", app.Name)
assert.Equal(t, "", app.Version)
}
// --- App via Core ---
func TestApp_Core_Good(t *testing.T) {
c := New(WithOption("name", "myapp")).Value.(*Core)
assert.Equal(t, "myapp", c.App().Name)
}
func TestApp_Empty_Good(t *testing.T) {
func TestApp_Core_Empty_Good(t *testing.T) {
c := New().Value.(*Core)
assert.NotNil(t, c.App())
assert.Equal(t, "", c.App().Name)
@ -26,14 +53,16 @@ func TestApp_Runtime_Good(t *testing.T) {
assert.NotNil(t, c.App().Runtime)
}
// --- App.Find ---
func TestApp_Find_Good(t *testing.T) {
r := Find("go", "go")
r := App{}.Find("go", "go")
assert.True(t, r.OK)
app := r.Value.(*App)
assert.NotEmpty(t, app.Path)
}
func TestApp_Find_Bad(t *testing.T) {
r := Find("nonexistent-binary-xyz", "test")
r := App{}.Find("nonexistent-binary-xyz", "test")
assert.False(t, r.OK)
}