feat: bootstrap cli entrypoint with bubble tea and toml config
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
const configFileName = "config.toml"
|
||||
|
||||
// Config is the user-owned TOML configuration for parasocial.
|
||||
type Config struct {
|
||||
Streamers []string `toml:"streamers"`
|
||||
}
|
||||
|
||||
// DefaultPath returns the config file path relative to the current working directory.
|
||||
func DefaultPath() string {
|
||||
return configFileName
|
||||
}
|
||||
|
||||
// LoadDefault reads config from the current working directory.
|
||||
func LoadDefault() (Config, error) {
|
||||
return Load(DefaultPath())
|
||||
}
|
||||
|
||||
// Load reads, normalizes, and validates a TOML config file.
|
||||
func Load(path string) (Config, error) {
|
||||
var cfg Config
|
||||
if _, err := toml.DecodeFile(path, &cfg); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return Config{}, fmt.Errorf("config file not found at %s", path)
|
||||
}
|
||||
return Config{}, fmt.Errorf("load config %s: %w", path, err)
|
||||
}
|
||||
|
||||
cfg.Streamers = normalizeStreamers(cfg.Streamers)
|
||||
if len(cfg.Streamers) == 0 {
|
||||
return Config{}, fmt.Errorf("config %s must define at least one streamer", path)
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func normalizeStreamers(streamers []string) []string {
|
||||
seen := make(map[string]struct{}, len(streamers))
|
||||
normalized := make([]string, 0, len(streamers))
|
||||
|
||||
for _, streamer := range streamers {
|
||||
streamer = normalizeStreamer(streamer)
|
||||
if streamer == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[streamer]; ok {
|
||||
continue
|
||||
}
|
||||
seen[streamer] = struct{}{}
|
||||
normalized = append(normalized, streamer)
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
func normalizeStreamer(streamer string) string {
|
||||
streamer = strings.TrimSpace(streamer)
|
||||
streamer = strings.TrimPrefix(streamer, "#")
|
||||
return strings.ToLower(strings.TrimSpace(streamer))
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadValidConfig(t *testing.T) {
|
||||
path := writeConfig(t, `streamers = [" Alpha ", "#Beta", "alpha", "", " #Gamma "]`)
|
||||
|
||||
cfg, err := Load(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
|
||||
want := []string{"alpha", "beta", "gamma"}
|
||||
if !reflect.DeepEqual(cfg.Streamers, want) {
|
||||
t.Fatalf("Streamers = %#v, want %#v", cfg.Streamers, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRejectsMissingStreamerList(t *testing.T) {
|
||||
path := writeConfig(t, `streamers = []`)
|
||||
|
||||
_, err := Load(path)
|
||||
if err == nil {
|
||||
t.Fatal("Load() error = nil, want error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "at least one streamer") {
|
||||
t.Fatalf("Load() error = %q, want empty streamer message", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRejectsInvalidTOML(t *testing.T) {
|
||||
path := writeConfig(t, `streamers = [`)
|
||||
|
||||
_, err := Load(path)
|
||||
if err == nil {
|
||||
t.Fatal("Load() error = nil, want error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "load config") {
|
||||
t.Fatalf("Load() error = %q, want load config context", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultPathUsesCurrentWorkingDirectoryConfig(t *testing.T) {
|
||||
if got := DefaultPath(); got != "config.toml" {
|
||||
t.Fatalf("DefaultPath() = %q, want %q", got, "config.toml")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRejectsMissingFile(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "missing.toml")
|
||||
|
||||
_, err := Load(path)
|
||||
if err == nil {
|
||||
t.Fatal("Load() error = nil, want error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "config file not found") {
|
||||
t.Fatalf("Load() error = %q, want missing file context", err)
|
||||
}
|
||||
}
|
||||
|
||||
func writeConfig(t *testing.T, content string) string {
|
||||
t.Helper()
|
||||
|
||||
path := filepath.Join(t.TempDir(), "config.toml")
|
||||
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
|
||||
t.Fatalf("write config: %v", err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
Reference in New Issue
Block a user