orbiton/
config.rs

1// Configuration system for Orbiton CLI
2// Supports .orbiton.toml configuration files for customizing build and dev behavior
3
4use anyhow::{Context, Result};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::fs;
8use std::path::{Path, PathBuf};
9
10/// Main configuration structure for Orbiton
11#[derive(Debug, Clone, Serialize, Deserialize, Default)]
12pub struct OrbitonConfig {
13    /// Project configuration
14    #[serde(default)]
15    pub project: ProjectConfig,
16
17    /// Development server configuration
18    #[serde(default)]
19    pub dev_server: DevServerConfig,
20
21    /// Hot Module Reload configuration
22    #[serde(default)]
23    pub hmr: HmrConfig,
24
25    /// Build configuration
26    #[serde(default)]
27    pub build: BuildConfig,
28
29    /// Linting configuration
30    #[serde(default)]
31    pub lint: LintConfig,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct ProjectConfig {
36    /// Project name
37    pub name: Option<String>,
38
39    /// Project version
40    pub version: Option<String>,
41
42    /// Source directory (default: "src")
43    #[serde(default = "default_src_dir")]
44    pub src_dir: String,
45
46    /// Output directory for builds (default: "dist")
47    #[serde(default = "default_dist_dir")]
48    pub dist_dir: String,
49
50    /// Entry point file (default: "main.rs")
51    #[serde(default = "default_entry_point")]
52    pub entry_point: String,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct DevServerConfig {
57    /// Port for the development server (default: 3000)
58    #[serde(default = "default_dev_port")]
59    pub port: u16,
60
61    /// Host for the development server (default: "127.0.0.1")
62    #[serde(default = "default_dev_host")]
63    pub host: String,
64
65    /// Whether to open browser automatically (default: true)
66    #[serde(default = "default_auto_open")]
67    pub auto_open: bool,
68
69    /// Additional static file directories to serve
70    #[serde(default)]
71    pub static_dirs: Vec<String>,
72
73    /// Custom headers to add to responses
74    #[serde(default)]
75    pub headers: HashMap<String, String>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct HmrConfig {
80    /// Whether HMR is enabled (default: true)
81    #[serde(default = "default_hmr_enabled")]
82    pub enabled: bool,
83
84    /// Debounce time in milliseconds for file changes (default: 100)
85    #[serde(default = "default_hmr_debounce")]
86    pub debounce_ms: u64,
87
88    /// Files and patterns to ignore during HMR
89    #[serde(default)]
90    pub ignore_patterns: Vec<String>,
91
92    /// Whether to preserve component state during HMR (default: true)
93    #[serde(default = "default_preserve_state")]
94    pub preserve_state: bool,
95
96    /// Maximum number of HMR retries before full reload (default: 3)
97    #[serde(default = "default_max_retries")]
98    pub max_retries: u32,
99
100    /// Whether to show HMR notifications in browser (default: true)
101    #[serde(default = "default_show_notifications")]
102    pub show_notifications: bool,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize, Default)]
106pub struct BuildConfig {
107    /// Whether to use beta Rust toolchain (default: false)
108    #[serde(default)]
109    pub use_beta_toolchain: bool,
110
111    /// Release mode for builds (default: false)
112    #[serde(default)]
113    pub release: bool,
114
115    /// Target triple for builds
116    pub target: Option<String>,
117
118    /// Additional build features to enable
119    #[serde(default)]
120    pub features: Vec<String>,
121
122    /// Build optimization level (0-3, s, z)
123    pub opt_level: Option<String>,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct LintConfig {
128    /// Whether linting is enabled (default: true)
129    #[serde(default = "default_lint_enabled")]
130    pub enabled: bool,
131
132    /// Lint rules to enable/disable
133    #[serde(default)]
134    pub rules: HashMap<String, bool>,
135
136    /// Custom lint configuration
137    #[serde(default)]
138    pub custom_rules: Vec<String>,
139}
140
141// Default value functions
142fn default_src_dir() -> String {
143    "src".to_string()
144}
145fn default_dist_dir() -> String {
146    "dist".to_string()
147}
148fn default_entry_point() -> String {
149    "main.rs".to_string()
150}
151fn default_dev_port() -> u16 {
152    3000
153}
154fn default_dev_host() -> String {
155    "127.0.0.1".to_string()
156}
157fn default_auto_open() -> bool {
158    true
159}
160fn default_hmr_enabled() -> bool {
161    true
162}
163fn default_hmr_debounce() -> u64 {
164    100
165}
166fn default_preserve_state() -> bool {
167    true
168}
169fn default_max_retries() -> u32 {
170    3
171}
172fn default_show_notifications() -> bool {
173    true
174}
175fn default_lint_enabled() -> bool {
176    true
177}
178
179impl Default for ProjectConfig {
180    fn default() -> Self {
181        Self {
182            name: None,
183            version: None,
184            src_dir: default_src_dir(),
185            dist_dir: default_dist_dir(),
186            entry_point: default_entry_point(),
187        }
188    }
189}
190
191impl Default for DevServerConfig {
192    fn default() -> Self {
193        Self {
194            port: default_dev_port(),
195            host: default_dev_host(),
196            auto_open: default_auto_open(),
197            static_dirs: vec![],
198            headers: HashMap::new(),
199        }
200    }
201}
202
203impl Default for HmrConfig {
204    fn default() -> Self {
205        Self {
206            enabled: default_hmr_enabled(),
207            debounce_ms: default_hmr_debounce(),
208            ignore_patterns: vec![
209                "target/**".to_string(),
210                "**/.git/**".to_string(),
211                "**/node_modules/**".to_string(),
212                "**/*.log".to_string(),
213            ],
214            preserve_state: default_preserve_state(),
215            max_retries: default_max_retries(),
216            show_notifications: default_show_notifications(),
217        }
218    }
219}
220
221impl Default for LintConfig {
222    fn default() -> Self {
223        Self {
224            enabled: default_lint_enabled(),
225            rules: HashMap::new(),
226            custom_rules: vec![],
227        }
228    }
229}
230
231impl OrbitonConfig {
232    /// Load configuration from a .orbiton.toml file
233    ///
234    /// Searches for the configuration file in the following order:
235    /// 1. Current directory
236    /// 2. Parent directories (walking up the tree)
237    /// 3. Uses default configuration if no file found
238    pub fn load_from_project(project_dir: &Path) -> Result<Self> {
239        let config_path = Self::find_config_file(project_dir);
240
241        match config_path {
242            Some(path) => Self::load_from_file(&path),
243            None => {
244                println!("No .orbiton.toml found, using default configuration");
245                Ok(Self::default())
246            }
247        }
248    }
249
250    /// Load configuration from a specific file
251    pub fn load_from_file(path: &Path) -> Result<Self> {
252        let content = fs::read_to_string(path)
253            .with_context(|| format!("Failed to read config file: {}", path.display()))?;
254
255        let config: OrbitonConfig = toml::from_str(&content)
256            .with_context(|| format!("Failed to parse config file: {}", path.display()))?;
257
258        println!("Loaded configuration from: {}", path.display());
259        Ok(config)
260    }
261
262    /// Find the nearest .orbiton.toml file by walking up the directory tree
263    pub fn find_config_file(start_dir: &Path) -> Option<PathBuf> {
264        let mut current_dir = start_dir;
265
266        loop {
267            let config_path = current_dir.join(".orbiton.toml");
268            if config_path.exists() {
269                return Some(config_path);
270            }
271
272            match current_dir.parent() {
273                Some(parent) => current_dir = parent,
274                None => return None,
275            }
276        }
277    }
278
279    /// Save configuration to a file
280    pub fn save_to_file(&self, path: &Path) -> Result<()> {
281        let content = toml::to_string_pretty(self).context("Failed to serialize configuration")?;
282
283        fs::write(path, content)
284            .with_context(|| format!("Failed to write config file: {}", path.display()))?;
285
286        println!("Configuration saved to: {}", path.display());
287        Ok(())
288    }
289
290    /// Create a default configuration file in the specified directory
291    pub fn create_default_config(project_dir: &Path) -> Result<PathBuf> {
292        let config_path = project_dir.join(".orbiton.toml");
293        let default_config = Self::default();
294
295        default_config.save_to_file(&config_path)?;
296        Ok(config_path)
297    }
298    /// Merge with another configuration (other takes precedence)
299    #[allow(dead_code)] // Used in tests and maintenance operations
300    pub fn merge_with(&mut self, other: &OrbitonConfig) {
301        // Merge project config
302        if other.project.name.is_some() {
303            self.project.name = other.project.name.clone();
304        }
305        if other.project.version.is_some() {
306            self.project.version = other.project.version.clone();
307        }
308
309        // Merge dev server config
310        if other.dev_server.port != default_dev_port() {
311            self.dev_server.port = other.dev_server.port;
312        }
313        if other.dev_server.host != default_dev_host() {
314            self.dev_server.host = other.dev_server.host.clone();
315        }
316
317        // Merge HMR config
318        if !other.hmr.enabled {
319            self.hmr.enabled = other.hmr.enabled;
320        }
321        if other.hmr.debounce_ms != default_hmr_debounce() {
322            self.hmr.debounce_ms = other.hmr.debounce_ms;
323        }
324
325        // Merge ignore patterns
326        if !other.hmr.ignore_patterns.is_empty() {
327            self.hmr
328                .ignore_patterns
329                .extend(other.hmr.ignore_patterns.clone());
330        }
331
332        // Merge build config
333        if other.build.use_beta_toolchain {
334            self.build.use_beta_toolchain = other.build.use_beta_toolchain;
335        }
336        if other.build.release {
337            self.build.release = other.build.release;
338        }
339
340        // Merge lint config
341        if !other.lint.enabled {
342            self.lint.enabled = other.lint.enabled;
343        }
344        for (rule, enabled) in &other.lint.rules {
345            self.lint.rules.insert(rule.clone(), *enabled);
346        }
347    }
348
349    /// Validate configuration and return any errors
350    pub fn validate(&self) -> Result<()> {
351        // Validate port range
352        if self.dev_server.port == 0 {
353            return Err(anyhow::anyhow!("Dev server port cannot be 0"));
354        }
355
356        // Validate paths exist
357        let src_path = Path::new(&self.project.src_dir);
358        if !src_path.exists() && self.project.src_dir != "src" {
359            return Err(anyhow::anyhow!(
360                "Source directory does not exist: {}",
361                self.project.src_dir
362            ));
363        }
364
365        // Validate HMR settings
366        if self.hmr.debounce_ms > 5000 {
367            return Err(anyhow::anyhow!(
368                "HMR debounce time too high (max 5000ms): {}",
369                self.hmr.debounce_ms
370            ));
371        }
372
373        Ok(())
374    }
375}
376
377#[cfg(test)]
378mod tests {
379    use super::*;
380    use tempfile::tempdir;
381
382    #[test]
383    fn test_default_config() {
384        let config = OrbitonConfig::default();
385        assert_eq!(config.dev_server.port, 3000);
386        assert_eq!(config.dev_server.host, "127.0.0.1");
387        assert!(config.hmr.enabled);
388        assert_eq!(config.hmr.debounce_ms, 100);
389    }
390
391    #[test]
392    fn test_config_serialization() {
393        let config = OrbitonConfig::default();
394        let toml_str = toml::to_string(&config).unwrap();
395        let parsed: OrbitonConfig = toml::from_str(&toml_str).unwrap();
396
397        assert_eq!(config.dev_server.port, parsed.dev_server.port);
398        assert_eq!(config.hmr.enabled, parsed.hmr.enabled);
399    }
400
401    #[test]
402    fn test_config_file_operations() {
403        let temp_dir = tempdir().unwrap();
404        let config_path = temp_dir.path().join(".orbiton.toml");
405
406        let config = OrbitonConfig::default();
407        config.save_to_file(&config_path).unwrap();
408
409        assert!(config_path.exists());
410
411        let loaded_config = OrbitonConfig::load_from_file(&config_path).unwrap();
412        assert_eq!(config.dev_server.port, loaded_config.dev_server.port);
413    }
414
415    #[test]
416    fn test_config_validation() {
417        let mut config = OrbitonConfig::default();
418        assert!(config.validate().is_ok());
419
420        config.dev_server.port = 0;
421        assert!(config.validate().is_err());
422
423        config.dev_server.port = 3000;
424        config.hmr.debounce_ms = 10000;
425        assert!(config.validate().is_err());
426    }
427
428    #[test]
429    fn test_config_merge() {
430        let mut base_config = OrbitonConfig::default();
431        let mut override_config = OrbitonConfig::default();
432
433        override_config.dev_server.port = 8080;
434        override_config.hmr.enabled = false;
435
436        base_config.merge_with(&override_config);
437
438        assert_eq!(base_config.dev_server.port, 8080);
439        assert!(!base_config.hmr.enabled);
440    }
441}