1use anyhow::{Context, Result};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::fs;
8use std::path::{Path, PathBuf};
9
10#[derive(Debug, Clone, Serialize, Deserialize, Default)]
12pub struct OrbitonConfig {
13 #[serde(default)]
15 pub project: ProjectConfig,
16
17 #[serde(default)]
19 pub dev_server: DevServerConfig,
20
21 #[serde(default)]
23 pub hmr: HmrConfig,
24
25 #[serde(default)]
27 pub build: BuildConfig,
28
29 #[serde(default)]
31 pub lint: LintConfig,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct ProjectConfig {
36 pub name: Option<String>,
38
39 pub version: Option<String>,
41
42 #[serde(default = "default_src_dir")]
44 pub src_dir: String,
45
46 #[serde(default = "default_dist_dir")]
48 pub dist_dir: String,
49
50 #[serde(default = "default_entry_point")]
52 pub entry_point: String,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct DevServerConfig {
57 #[serde(default = "default_dev_port")]
59 pub port: u16,
60
61 #[serde(default = "default_dev_host")]
63 pub host: String,
64
65 #[serde(default = "default_auto_open")]
67 pub auto_open: bool,
68
69 #[serde(default)]
71 pub static_dirs: Vec<String>,
72
73 #[serde(default)]
75 pub headers: HashMap<String, String>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct HmrConfig {
80 #[serde(default = "default_hmr_enabled")]
82 pub enabled: bool,
83
84 #[serde(default = "default_hmr_debounce")]
86 pub debounce_ms: u64,
87
88 #[serde(default)]
90 pub ignore_patterns: Vec<String>,
91
92 #[serde(default = "default_preserve_state")]
94 pub preserve_state: bool,
95
96 #[serde(default = "default_max_retries")]
98 pub max_retries: u32,
99
100 #[serde(default = "default_show_notifications")]
102 pub show_notifications: bool,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize, Default)]
106pub struct BuildConfig {
107 #[serde(default)]
109 pub use_beta_toolchain: bool,
110
111 #[serde(default)]
113 pub release: bool,
114
115 pub target: Option<String>,
117
118 #[serde(default)]
120 pub features: Vec<String>,
121
122 pub opt_level: Option<String>,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct LintConfig {
128 #[serde(default = "default_lint_enabled")]
130 pub enabled: bool,
131
132 #[serde(default)]
134 pub rules: HashMap<String, bool>,
135
136 #[serde(default)]
138 pub custom_rules: Vec<String>,
139}
140
141fn 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 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 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 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 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 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 #[allow(dead_code)] pub fn merge_with(&mut self, other: &OrbitonConfig) {
301 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 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 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 if !other.hmr.ignore_patterns.is_empty() {
327 self.hmr
328 .ignore_patterns
329 .extend(other.hmr.ignore_patterns.clone());
330 }
331
332 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 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 pub fn validate(&self) -> Result<()> {
351 if self.dev_server.port == 0 {
353 return Err(anyhow::anyhow!("Dev server port cannot be 0"));
354 }
355
356 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 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}