1package config
2
3import (
4 "fmt"
5 "strings"
6
7 domainvalidation "github.com/bnema/dumber/internal/domain/validation"
8)
9
10// validateConfig performs comprehensive validation of configuration values
11func validateConfig(config *Config) error {
12 var validationErrors []string
13
14 validationErrors = append(validationErrors, validateHistory(config)...)
15 validationErrors = append(validationErrors, validateDmenu(config)...)
16 validationErrors = append(validationErrors, validateAppearance(config)...)
17 validationErrors = append(validationErrors, validateSearchEngine(config)...)
18 validationErrors = append(validationErrors, validatePopups(config)...)
19 validationErrors = append(validationErrors, validateWorkspaceStyling(config)...)
20 validationErrors = append(validationErrors, validatePaneMode(config)...)
21 validationErrors = append(validationErrors, validateTabBar(config)...)
22 validationErrors = append(validationErrors, validateTabMode(config)...)
23 validationErrors = append(validationErrors, validateLogging(config)...)
24 validationErrors = append(validationErrors, validateOmnibox(config)...)
25 validationErrors = append(validationErrors, validateRendering(config)...)
26 validationErrors = append(validationErrors, validateColorScheme(config)...)
27
28 // If there are validation errors, return them
29 if len(validationErrors) > 0 {
30 return fmt.Errorf("config validation failed:\n - %s", strings.Join(validationErrors, "\n - "))
31 }
32
33 return nil
34}
35
36func validateHistory(config *Config) []string {
37 var validationErrors []string
38 if config.History.MaxEntries < 0 {
39 validationErrors = append(validationErrors, "history.max_entries must be non-negative")
40 }
41 if config.History.RetentionPeriodDays < 0 {
42 validationErrors = append(validationErrors, "history.retention_period_days must be non-negative")
43 }
44 if config.History.CleanupIntervalDays < 0 {
45 validationErrors = append(validationErrors, "history.cleanup_interval_days must be non-negative")
46 }
47 return validationErrors
48}
49
50func validateDmenu(config *Config) []string {
51 if config.Dmenu.MaxHistoryItems < 0 {
52 return []string{"dmenu.max_history_items must be non-negative"}
53 }
54 return nil
55}
56
57func validateAppearance(config *Config) []string {
58 var validationErrors []string
59 if config.Appearance.DefaultFontSize < 1 || config.Appearance.DefaultFontSize > 72 {
60 validationErrors = append(validationErrors, "appearance.default_font_size must be between 1 and 72")
61 }
62 validationErrors = append(validationErrors, domainvalidation.ValidateFontFamily("appearance.sans_font", config.Appearance.SansFont)...)
63 validationErrors = append(validationErrors, domainvalidation.ValidateFontFamily("appearance.serif_font", config.Appearance.SerifFont)...)
64 validationErrors = append(
65 validationErrors,
66 domainvalidation.ValidateFontFamily("appearance.monospace_font", config.Appearance.MonospaceFont)...,
67 )
68 validationErrors = append(validationErrors, domainvalidation.ValidatePaletteHex(
69 "appearance.light_palette",
70 config.Appearance.LightPalette.Background,
71 config.Appearance.LightPalette.Surface,
72 config.Appearance.LightPalette.SurfaceVariant,
73 config.Appearance.LightPalette.Text,
74 config.Appearance.LightPalette.Muted,
75 config.Appearance.LightPalette.Accent,
76 config.Appearance.LightPalette.Border,
77 )...)
78 validationErrors = append(validationErrors, domainvalidation.ValidatePaletteHex(
79 "appearance.dark_palette",
80 config.Appearance.DarkPalette.Background,
81 config.Appearance.DarkPalette.Surface,
82 config.Appearance.DarkPalette.SurfaceVariant,
83 config.Appearance.DarkPalette.Text,
84 config.Appearance.DarkPalette.Muted,
85 config.Appearance.DarkPalette.Accent,
86 config.Appearance.DarkPalette.Border,
87 )...)
88 if config.DefaultWebpageZoom < 0.1 || config.DefaultWebpageZoom > 5.0 {
89 validationErrors = append(validationErrors, "default_webpage_zoom must be between 0.1 and 5.0")
90 }
91 if config.DefaultUIScale < 0.5 || config.DefaultUIScale > 3.0 {
92 validationErrors = append(validationErrors, "default_ui_scale must be between 0.5 and 3.0")
93 }
94 return validationErrors
95}
96
97func validateSearchEngine(config *Config) []string {
98 if config.DefaultSearchEngine == "" {
99 return []string{"default_search_engine cannot be empty"}
100 }
101 if !strings.Contains(config.DefaultSearchEngine, "%s") {
102 return []string{"default_search_engine must contain %s placeholder for the search query"}
103 }
104 return nil
105}
106
107func validatePopups(config *Config) []string {
108 var validationErrors []string
109 switch config.Workspace.Popups.Behavior {
110 case PopupBehaviorSplit, PopupBehaviorStacked, PopupBehaviorTabbed, PopupBehaviorWindowed:
111 default:
112 validationErrors = append(validationErrors, fmt.Sprintf(
113 "workspace.popups.behavior must be one of: split, stacked, tabbed, windowed (got: %s)",
114 config.Workspace.Popups.Behavior,
115 ))
116 }
117
118 if config.Workspace.Popups.Behavior == PopupBehaviorSplit {
119 switch config.Workspace.Popups.Placement {
120 case "right", "left", "top", "bottom":
121 default:
122 validationErrors = append(validationErrors, fmt.Sprintf(
123 "workspace.popups.placement must be one of: right, left, top, bottom (got: %s)",
124 config.Workspace.Popups.Placement,
125 ))
126 }
127 }
128
129 switch config.Workspace.Popups.BlankTargetBehavior {
130 case "split", "stacked", "tabbed":
131 default:
132 validationErrors = append(validationErrors, fmt.Sprintf(
133 "workspace.popups.blank_target_behavior must be one of: split, stacked, tabbed (got: %s)",
134 config.Workspace.Popups.BlankTargetBehavior,
135 ))
136 }
137 return validationErrors
138}
139
140func validateWorkspaceStyling(config *Config) []string {
141 var validationErrors []string
142 if config.Workspace.Styling.BorderWidth < 0 {
143 validationErrors = append(validationErrors, "workspace.styling.border_width must be non-negative")
144 }
145 if config.Workspace.Styling.PaneModeBorderWidth < 0 {
146 validationErrors = append(validationErrors, "workspace.styling.pane_mode_border_width must be non-negative")
147 }
148 if config.Workspace.Styling.TabModeBorderWidth < 0 {
149 validationErrors = append(validationErrors, "workspace.styling.tab_mode_border_width must be non-negative")
150 }
151 if config.Workspace.Styling.TransitionDuration < 0 {
152 validationErrors = append(validationErrors, "workspace.styling.transition_duration must be non-negative")
153 }
154 return validationErrors
155}
156
157func validatePaneMode(config *Config) []string {
158 var validationErrors []string
159 if config.Workspace.PaneMode.TimeoutMilliseconds < 0 {
160 validationErrors = append(validationErrors, "workspace.pane_mode.timeout_ms must be non-negative")
161 }
162 if len(config.Workspace.PaneMode.Actions) == 0 {
163 validationErrors = append(validationErrors, "workspace.pane_mode.actions cannot be empty")
164 }
165
166 seenKeys := make(map[string]string)
167 for action, keys := range config.Workspace.PaneMode.Actions {
168 if len(keys) == 0 {
169 validationErrors = append(validationErrors, fmt.Sprintf("workspace.pane_mode.actions.%s must have at least one key binding", action))
170 }
171 for _, key := range keys {
172 if existingAction, exists := seenKeys[key]; exists {
173 validationErrors = append(validationErrors, fmt.Sprintf(
174 "duplicate key binding '%s' found in pane_mode actions '%s' and '%s'",
175 key,
176 existingAction,
177 action,
178 ))
179 }
180 seenKeys[key] = action
181 }
182 }
183 return validationErrors
184}
185
186func validateTabBar(config *Config) []string {
187 switch config.Workspace.TabBarPosition {
188 case "top", "bottom":
189 return nil
190 default:
191 return []string{fmt.Sprintf("workspace.tab_bar_position must be 'top' or 'bottom' (got: %s)", config.Workspace.TabBarPosition)}
192 }
193}
194
195func validateTabMode(config *Config) []string {
196 var validationErrors []string
197 if config.Workspace.TabMode.TimeoutMilliseconds < 0 {
198 validationErrors = append(validationErrors, "workspace.tab_mode.timeout_ms must be non-negative")
199 }
200 if len(config.Workspace.TabMode.Actions) == 0 {
201 validationErrors = append(validationErrors, "workspace.tab_mode.actions cannot be empty")
202 }
203
204 tabSeenKeys := make(map[string]string)
205 for action, keys := range config.Workspace.TabMode.Actions {
206 if len(keys) == 0 {
207 validationErrors = append(validationErrors, fmt.Sprintf("workspace.tab_mode.actions.%s must have at least one key binding", action))
208 }
209 for _, key := range keys {
210 if existingAction, exists := tabSeenKeys[key]; exists {
211 validationErrors = append(validationErrors, fmt.Sprintf(
212 "duplicate key binding '%s' found in tab_mode actions '%s' and '%s'",
213 key,
214 existingAction,
215 action,
216 ))
217 }
218 tabSeenKeys[key] = action
219 }
220 }
221 return validationErrors
222}
223
224func validateLogging(config *Config) []string {
225 var validationErrors []string
226 if config.Logging.MaxAge < 0 {
227 validationErrors = append(validationErrors, "logging.max_age must be non-negative")
228 }
229 switch config.Logging.Level {
230 case "trace", "debug", "info", "warn", "error", "fatal", "":
231 default:
232 validationErrors = append(validationErrors, fmt.Sprintf(
233 "logging.level must be one of: trace, debug, info, warn, error, fatal (got: %s)",
234 config.Logging.Level,
235 ))
236 }
237 switch config.Logging.Format {
238 case "text", "json", "console", "":
239 default:
240 validationErrors = append(validationErrors, fmt.Sprintf(
241 "logging.format must be one of: text, json, console (got: %s)",
242 config.Logging.Format,
243 ))
244 }
245 return validationErrors
246}
247
248func validateOmnibox(config *Config) []string {
249 switch config.Omnibox.InitialBehavior {
250 case "recent", "most_visited", "none":
251 return nil
252 default:
253 return []string{fmt.Sprintf(
254 "omnibox.initial_behavior must be one of: recent, most_visited, none (got: %s)",
255 config.Omnibox.InitialBehavior,
256 )}
257 }
258}
259
260func validateRendering(config *Config) []string {
261 var validationErrors []string
262
263 switch config.Rendering.Mode {
264 case RenderingModeAuto, RenderingModeGPU, RenderingModeCPU, "":
265 default:
266 validationErrors = append(
267 validationErrors,
268 fmt.Sprintf("rendering.mode must be one of: auto, gpu, cpu (got: %s)", config.Rendering.Mode),
269 )
270 }
271
272 switch config.Rendering.GSKRenderer {
273 case GSKRendererAuto, GSKRendererOpenGL, GSKRendererVulkan, GSKRendererCairo, "":
274 default:
275 validationErrors = append(
276 validationErrors,
277 fmt.Sprintf(
278 "rendering.gsk_renderer must be one of: auto, opengl, vulkan, cairo (got: %s)",
279 config.Rendering.GSKRenderer,
280 ),
281 )
282 }
283
284 if config.Rendering.ForceCompositingMode && config.Rendering.DisableCompositingMode {
285 validationErrors = append(validationErrors,
286 "rendering.force_compositing_mode and rendering.disable_compositing_mode cannot both be true",
287 )
288 }
289
290 return validationErrors
291}
292
293func validateColorScheme(config *Config) []string {
294 switch config.Appearance.ColorScheme {
295 case ThemePreferDark, ThemePreferLight, ThemeDefault, "":
296 return nil
297 default:
298 return []string{fmt.Sprintf(
299 "appearance.color_scheme must be one of: prefer-dark, prefer-light, default (got: %s)",
300 config.Appearance.ColorScheme,
301 )}
302 }
303}