1package usecase
2
3import (
4 "context"
5 "fmt"
6 "strings"
7
8 "github.com/bnema/dumber/internal/application/port"
9 "github.com/bnema/dumber/internal/domain/validation"
10)
11
12type SaveWebUIConfigUseCase struct {
13 saver port.WebUIConfigSaver
14}
15
16func NewSaveWebUIConfigUseCase(saver port.WebUIConfigSaver) *SaveWebUIConfigUseCase {
17 return &SaveWebUIConfigUseCase{saver: saver}
18}
19
20func (uc *SaveWebUIConfigUseCase) Execute(ctx context.Context, cfg port.WebUIConfig) error {
21 if uc == nil || uc.saver == nil {
22 return fmt.Errorf("config saver is nil")
23 }
24
25 normalized := normalizeWebUIConfig(cfg)
26 if err := validateWebUIConfig(normalized); err != nil {
27 return err
28 }
29
30 return uc.saver.SaveWebUIConfig(ctx, normalized)
31}
32
33func normalizeWebUIConfig(cfg port.WebUIConfig) port.WebUIConfig {
34 cfg.Appearance.SansFont = strings.TrimSpace(cfg.Appearance.SansFont)
35 cfg.Appearance.SerifFont = strings.TrimSpace(cfg.Appearance.SerifFont)
36 cfg.Appearance.MonospaceFont = strings.TrimSpace(cfg.Appearance.MonospaceFont)
37 cfg.Appearance.ColorScheme = strings.TrimSpace(cfg.Appearance.ColorScheme)
38 cfg.DefaultSearchEngine = strings.TrimSpace(cfg.DefaultSearchEngine)
39 if len(cfg.SearchShortcuts) > 0 {
40 normalized := make(map[string]port.SearchShortcut, len(cfg.SearchShortcuts))
41 for key, shortcut := range cfg.SearchShortcuts {
42 trimmedKey := strings.TrimSpace(key)
43 shortcut.URL = strings.TrimSpace(shortcut.URL)
44 shortcut.Description = strings.TrimSpace(shortcut.Description)
45 if trimmedKey != "" {
46 normalized[trimmedKey] = shortcut
47 }
48 }
49 cfg.SearchShortcuts = normalized
50 }
51 return cfg
52}
53
54func validateWebUIConfig(cfg port.WebUIConfig) error {
55 var errs []string
56
57 if cfg.Appearance.DefaultFontSize < 1 || cfg.Appearance.DefaultFontSize > 72 {
58 errs = append(errs, "appearance.default_font_size must be between 1 and 72")
59 }
60 if cfg.DefaultUIScale < 0.5 || cfg.DefaultUIScale > 3.0 {
61 errs = append(errs, "default_ui_scale must be between 0.5 and 3.0")
62 }
63
64 errs = append(errs, validation.ValidateFontFamily("appearance.sans_font", cfg.Appearance.SansFont)...)
65 errs = append(errs, validation.ValidateFontFamily("appearance.serif_font", cfg.Appearance.SerifFont)...)
66 errs = append(errs, validation.ValidateFontFamily("appearance.monospace_font", cfg.Appearance.MonospaceFont)...)
67
68 if cfg.DefaultSearchEngine == "" {
69 errs = append(errs, "default_search_engine cannot be empty")
70 } else if !strings.Contains(cfg.DefaultSearchEngine, "%s") {
71 errs = append(errs, "default_search_engine must contain %s placeholder for the search query")
72 }
73
74 for key, shortcut := range cfg.SearchShortcuts {
75 for _, err := range validation.ValidateShortcutKey(key) {
76 errs = append(errs, fmt.Sprintf("search_shortcuts.%s: %s", key, err))
77 }
78 for _, err := range validation.ValidateShortcutURL(shortcut.URL) {
79 errs = append(errs, fmt.Sprintf("search_shortcuts.%s.url: %s", key, err))
80 }
81 for _, err := range validation.ValidateShortcutDescription(shortcut.Description) {
82 errs = append(errs, fmt.Sprintf("search_shortcuts.%s.description: %s", key, err))
83 }
84 }
85
86 switch cfg.Appearance.ColorScheme {
87 case "default", "prefer-dark", "prefer-light":
88 // ok
89 default:
90 errs = append(errs, fmt.Sprintf(
91 "appearance.color_scheme must be one of: prefer-dark, prefer-light, default (got: %s)",
92 cfg.Appearance.ColorScheme,
93 ))
94 }
95
96 errs = append(errs, validation.ValidatePaletteHex(
97 "appearance.light_palette",
98 cfg.Appearance.LightPalette.Background,
99 cfg.Appearance.LightPalette.Surface,
100 cfg.Appearance.LightPalette.SurfaceVariant,
101 cfg.Appearance.LightPalette.Text,
102 cfg.Appearance.LightPalette.Muted,
103 cfg.Appearance.LightPalette.Accent,
104 cfg.Appearance.LightPalette.Border,
105 )...)
106 errs = append(errs, validation.ValidatePaletteHex(
107 "appearance.dark_palette",
108 cfg.Appearance.DarkPalette.Background,
109 cfg.Appearance.DarkPalette.Surface,
110 cfg.Appearance.DarkPalette.SurfaceVariant,
111 cfg.Appearance.DarkPalette.Text,
112 cfg.Appearance.DarkPalette.Muted,
113 cfg.Appearance.DarkPalette.Accent,
114 cfg.Appearance.DarkPalette.Border,
115 )...)
116
117 if len(errs) > 0 {
118 return fmt.Errorf("config validation failed:\n - %s", strings.Join(errs, "\n - "))
119 }
120 return nil
121}