diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-06 10:58:58 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-06 10:58:58 +0300 |
| commit | 23160bce9a18a70080a85dda6e9c654499aba7f7 (patch) | |
| tree | ec0f9930233aef96a221980310073081679bb4bb /internal/processor/processor.go | |
| parent | 95dd36d28d18615ad3f8dd7122a404850dcb39f8 (diff) | |
refactor: remove Viper coupling from processor package (DIP)
Resolve all Viper config values once at startup in cmd/totalrecall/main.go
via the new newProcessorConfig() helper. The exported processor.Config struct
replaces the internal viperConfig type, and NewProcessor now accepts *Config
instead of querying Viper internally. The processor package no longer imports
Viper at all. Tests are updated to pass Config values directly, eliminating
the viper.Set/Reset boilerplate that coupled tests to the global singleton.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/processor/processor.go')
| -rw-r--r-- | internal/processor/processor.go | 141 |
1 files changed, 52 insertions, 89 deletions
diff --git a/internal/processor/processor.go b/internal/processor/processor.go index 107fd12..7d83149 100644 --- a/internal/processor/processor.go +++ b/internal/processor/processor.go @@ -9,8 +9,6 @@ import ( "strings" "time" - "github.com/spf13/viper" - "codeberg.org/snonux/totalrecall/internal" "codeberg.org/snonux/totalrecall/internal/anki" "codeberg.org/snonux/totalrecall/internal/audio" @@ -22,80 +20,45 @@ import ( "codeberg.org/snonux/totalrecall/internal/translation" ) -// viperConfig holds all Viper-sourced settings captured once in NewProcessor. -// Storing them in a struct avoids repeated global Viper access in method bodies -// and makes the values testable without mutating process-wide Viper state. -type viperConfig struct { +// Config holds all configuration-file values resolved once at startup by the +// composition root (cmd/totalrecall/main.go via cli.NewProcessorConfig). +// Keeping config in a dedicated struct means the processor package never +// imports or queries Viper directly, which improves testability and removes +// tight coupling to the global Viper singleton. +type Config struct { // Translation & phonetic settings - translationProvider string - phoneticProvider string - translationGeminiModel string + TranslationProvider string + PhoneticProvider string + TranslationGeminiModel string // Audio settings - audioProvider string - audioFormat string - audioFormatSet bool - geminiTTSModel string - geminiVoice string - openAIVoice string - openAIModel string - openAIModelSet bool - openAISpeed float64 - openAISpeedSet bool - openAIInstruction string - openAIInstructionSet bool + AudioProvider string + AudioFormat string + AudioFormatSet bool + GeminiTTSModel string + GeminiVoice string + OpenAIVoice string + OpenAIModel string + OpenAIModelSet bool + OpenAISpeed float64 + OpenAISpeedSet bool + OpenAIInstruction string + OpenAIInstructionSet bool // Image settings - imageProvider string - imageOpenAIModel string - imageOpenAIModelSet bool - imageOpenAISize string - imageOpenAISizeSet bool - imageOpenAIQuality string - imageOpenAIQualitySet bool - imageOpenAIStyle string - imageOpenAIStyleSet bool - imageNanoBananaModel string - imageNanoBananaModelSet bool - imageNanoBananaTextModel string - imageNanoBananaTextModelSet bool -} - -// newViperConfig reads all Viper settings in one pass. Called once from NewProcessor -// so the processor methods never touch the global Viper instance directly. -func newViperConfig() viperConfig { - return viperConfig{ - translationProvider: strings.TrimSpace(viper.GetString("translation.provider")), - phoneticProvider: strings.TrimSpace(viper.GetString("phonetic.provider")), - translationGeminiModel: viper.GetString("translation.gemini_model"), - - audioProvider: strings.ToLower(strings.TrimSpace(viper.GetString("audio.provider"))), - audioFormat: strings.ToLower(strings.TrimSpace(viper.GetString("audio.format"))), - audioFormatSet: viper.IsSet("audio.format"), - geminiTTSModel: strings.TrimSpace(viper.GetString("audio.gemini_tts_model")), - geminiVoice: strings.TrimSpace(viper.GetString("audio.gemini_voice")), - openAIVoice: strings.TrimSpace(viper.GetString("audio.openai_voice")), - openAIModel: viper.GetString("audio.openai_model"), - openAIModelSet: viper.IsSet("audio.openai_model"), - openAISpeed: viper.GetFloat64("audio.openai_speed"), - openAISpeedSet: viper.IsSet("audio.openai_speed"), - openAIInstruction: viper.GetString("audio.openai_instruction"), - openAIInstructionSet: viper.IsSet("audio.openai_instruction"), - - imageProvider: strings.ToLower(strings.TrimSpace(viper.GetString("image.provider"))), - imageOpenAIModel: viper.GetString("image.openai_model"), - imageOpenAIModelSet: viper.IsSet("image.openai_model"), - imageOpenAISize: viper.GetString("image.openai_size"), - imageOpenAISizeSet: viper.IsSet("image.openai_size"), - imageOpenAIQuality: viper.GetString("image.openai_quality"), - imageOpenAIQualitySet: viper.IsSet("image.openai_quality"), - imageOpenAIStyle: viper.GetString("image.openai_style"), - imageOpenAIStyleSet: viper.IsSet("image.openai_style"), - imageNanoBananaModel: strings.TrimSpace(viper.GetString("image.nanobanana_model")), - imageNanoBananaModelSet: viper.IsSet("image.nanobanana_model"), - imageNanoBananaTextModel: strings.TrimSpace(viper.GetString("image.nanobanana_text_model")), - imageNanoBananaTextModelSet: viper.IsSet("image.nanobanana_text_model"), - } + ImageProvider string + ImageOpenAIModel string + ImageOpenAIModelSet bool + ImageOpenAISize string + ImageOpenAISizeSet bool + ImageOpenAIQuality string + ImageOpenAIQualitySet bool + ImageOpenAIStyle string + ImageOpenAIStyleSet bool + ImageNanoBananaModel string + ImageNanoBananaModelSet bool + ImageNanoBananaTextModel string + ImageNanoBananaTextModelSet bool } // Processor handles the main word processing logic. @@ -109,9 +72,9 @@ type Processor struct { translationCache *translation.TranslationCache phoneticFetcher *phonetic.Fetcher randomIntn func(n int) int - // viperCfg holds all config-file values read once at construction time, + // cfg holds all config-file values resolved once at startup by the caller, // so individual methods never call Viper directly. - viperCfg viperConfig + cfg *Config // Factories — replaced by tests to inject fakes. newOpenAIImageClient func(*image.OpenAIConfig) image.ImageClient @@ -120,18 +83,18 @@ type Processor struct { } // NewProcessor creates a new word processor with default production factories. -// All Viper config values are read once here via newViperConfig() so that no -// method body ever calls Viper directly. +// cfg must be fully resolved before calling NewProcessor; the composition root +// (cmd/totalrecall/main.go) builds it via cli.NewProcessorConfig() so that +// the processor package never imports or queries Viper. // Tests can replace the factory fields on the returned struct to inject fakes. -func NewProcessor(flags *cli.Flags) *Processor { - cfg := newViperConfig() +func NewProcessor(flags *cli.Flags, cfg *Config) *Processor { openAIKey := cli.GetOpenAIKey() googleAPIKey := cli.GetGoogleAPIKey() - translationProvider := translation.Provider(cfg.translationProvider) - phoneticProvider := phonetic.Provider(cfg.phoneticProvider) + translationProvider := translation.Provider(cfg.TranslationProvider) + phoneticProvider := phonetic.Provider(cfg.PhoneticProvider) return &Processor{ flags: flags, - viperCfg: cfg, + cfg: cfg, translator: translation.NewTranslator(&translation.Config{Provider: translationProvider, OpenAIKey: openAIKey, GoogleAPIKey: googleAPIKey}), translationCache: translation.NewTranslationCache(), phoneticFetcher: phonetic.NewFetcher(&phonetic.Config{Provider: phoneticProvider, OpenAIKey: openAIKey, GoogleAPIKey: googleAPIKey}), @@ -501,8 +464,8 @@ func (p *Processor) GUIConfig() *gui.Config { openAIKey := cli.GetOpenAIKey() googleAPIKey := cli.GetGoogleAPIKey() - translationProvider := translation.Provider(p.viperCfg.translationProvider) - phoneticProvider := phonetic.Provider(p.viperCfg.phoneticProvider) + translationProvider := translation.Provider(p.cfg.TranslationProvider) + phoneticProvider := phonetic.Provider(p.cfg.PhoneticProvider) // Construct and inject phonetic/translation dependencies at the composition // root so gui.New() receives ready-to-use instances rather than raw config strings. @@ -514,7 +477,7 @@ func (p *Processor) GUIConfig() *gui.Config { translator := translation.NewTranslator(&translation.Config{ Provider: translationProvider, OpenAIKey: openAIKey, - GeminiModel: p.viperCfg.translationGeminiModel, + GeminiModel: p.cfg.TranslationGeminiModel, }) return &gui.Config{ @@ -536,7 +499,7 @@ func (p *Processor) GUIConfig() *gui.Config { } // nanoBananaModelForRunMode resolves the NanoBanana image model, preferring -// the explicit CLI flag value when set, then the viper config value, then the +// the explicit CLI flag value when set, then the config-file value, then the // package default. func (p *Processor) nanoBananaModelForRunMode() string { if p != nil && p.flags != nil && p.flags.NanoBananaModelSpecified { @@ -544,8 +507,8 @@ func (p *Processor) nanoBananaModelForRunMode() string { return model } } - if p.viperCfg.imageNanoBananaModel != "" { - return p.viperCfg.imageNanoBananaModel + if p.cfg.ImageNanoBananaModel != "" { + return p.cfg.ImageNanoBananaModel } if p != nil && p.flags != nil { if model := strings.TrimSpace(p.flags.NanoBananaModel); model != "" { @@ -556,15 +519,15 @@ func (p *Processor) nanoBananaModelForRunMode() string { } // nanoBananaTextModelForRunMode resolves the NanoBanana text (prompt) model -// using the same precedence as nanoBananaModelForRunMode. +// using the same CLI-flag-over-config precedence as nanoBananaModelForRunMode. func (p *Processor) nanoBananaTextModelForRunMode() string { if p != nil && p.flags != nil && p.flags.NanoBananaTextModelSpecified { if model := strings.TrimSpace(p.flags.NanoBananaTextModel); model != "" { return model } } - if p.viperCfg.imageNanoBananaTextModel != "" { - return p.viperCfg.imageNanoBananaTextModel + if p.cfg.ImageNanoBananaTextModel != "" { + return p.cfg.ImageNanoBananaTextModel } if p != nil && p.flags != nil { if model := strings.TrimSpace(p.flags.NanoBananaTextModel); model != "" { |
