feat(config): geschichtete Konfigurationsauflösung mit injizierbaren Pfaden
Defaults → optionale config.toml → Env-Overrides (TALK_AUDIO_SINK); Validierungsfehler sind klar, nie still. Plattform-Pfadlogik (linux/darwin/windows, XDG_CONFIG_HOME, HOME, APPDATA) mit goos als Parameter — alle Plattformen tischgesteuert vom Dev-Host testbar. Erste externe Dependency: github.com/BurntSushi/toml v1.6.0 (MIT, pure Go).
This commit is contained in:
1 parent
de1ef64b8f
commit
36ad1dfa4e
6 files changed
+363
-3
No files matched your search
@@ -0,0 +1,94 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
// Erlaubte Werte für audio.sink (Teil des öffentlichen Vertrags; Spec:
|
||||
// „Audio-Sink wählbar inkl. null").
|
||||
const (
|
||||
SinkOto = "oto"
|
||||
SinkNull = "null"
|
||||
)
|
||||
|
||||
// Config ist der effektiv geladene Konfigurations-Stand von talk. Task 02
|
||||
// führt nur audio.sink; spätere Tasks ergänzen Felder (Provider-Endpunkte,
|
||||
// Voice-Mapping, server, queue …), ohne die Auflösungsreihenfolge zu ändern.
|
||||
type Config struct {
|
||||
Audio Audio `toml:"audio"`
|
||||
}
|
||||
|
||||
// Audio steuert die Audio-Ausgabe.
|
||||
type Audio struct {
|
||||
// Sink wählt die Wiedergabe: "oto" (embedded Playback, Default) oder
|
||||
// "null" (verwirft — CI, Server, Tests).
|
||||
Sink string `toml:"sink"`
|
||||
}
|
||||
|
||||
// Defaults liefert die eingebaute Default-Konfiguration (unterste Schicht
|
||||
// der Auflösung).
|
||||
func Defaults() Config {
|
||||
return Config{Audio: Audio{Sink: SinkOto}}
|
||||
}
|
||||
|
||||
// Options steuert Load. Alle Felder sind injizierbar, damit die Unit-Tests
|
||||
// ohne echte HOME-/Env-Umgebung und ohne echte Dateien auskommen.
|
||||
type Options struct {
|
||||
// File überschreibt den platform-nativen Pfad der config.toml (Tests
|
||||
// injizieren hier eine Temp-Datei). Leer: Dir() + "config.toml".
|
||||
File string
|
||||
|
||||
// Getenv liefert Umgebungsvariablen (Default: os.Getenv). Über TALK_*
|
||||
// laufen die Env-Overrides sowie die Pfadauflösung (HOME,
|
||||
// XDG_CONFIG_HOME, APPDATA).
|
||||
Getenv func(string) string
|
||||
}
|
||||
|
||||
// Load löst die Konfiguration schichtweise auf: Defaults → config.toml
|
||||
// (falls vorhanden) → Env-Overrides (TALK_AUDIO_SINK schlägt die Datei).
|
||||
// Env- und Datei-Validierungsfehler werden als klarer Fehler gemeldet, nie
|
||||
// still ignoriert. Fehlt das Config-Verzeichnis (z. B. keine HOME-Variable),
|
||||
// läuft talk mit Defaults + Env weiter — die Datei ist optional, kaputte
|
||||
// Umgebungen dürfen talk nicht unbrauchbar machen.
|
||||
func Load(opts Options) (Config, error) {
|
||||
if opts.Getenv == nil {
|
||||
opts.Getenv = os.Getenv
|
||||
}
|
||||
cfg := Defaults()
|
||||
|
||||
path := opts.File
|
||||
if path == "" {
|
||||
dir, err := Dir(opts.Getenv)
|
||||
if err != nil {
|
||||
// Kein auffindbares Config-Verzeichnis: optionale Datei entfällt.
|
||||
return applyEnv(cfg, opts.Getenv)
|
||||
}
|
||||
path = filepath.Join(dir, fileName)
|
||||
}
|
||||
|
||||
if _, err := toml.DecodeFile(path, &cfg); err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return cfg, fmt.Errorf("config %s: %w", path, err)
|
||||
}
|
||||
}
|
||||
return applyEnv(cfg, opts.Getenv)
|
||||
}
|
||||
|
||||
// applyEnv wendet die Env-Overrides an und validiert das Ergebnis.
|
||||
func applyEnv(cfg Config, getenv func(string) string) (Config, error) {
|
||||
if v := getenv("TALK_AUDIO_SINK"); v != "" {
|
||||
cfg.Audio.Sink = v
|
||||
}
|
||||
switch cfg.Audio.Sink {
|
||||
case SinkOto, SinkNull:
|
||||
return cfg, nil
|
||||
default:
|
||||
return cfg, fmt.Errorf("audio.sink: ungültiger Wert %q (erlaubt: %q, %q)",
|
||||
cfg.Audio.Sink, SinkOto, SinkNull)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user