Skip to main content

netrunner_core/
settings.rs

1//! User settings, persisted as human-editable JSON.
2//!
3//! Settings are small and rarely written, so JSON is a better fit than the
4//! embedded database used for [`crate::history`]. The file lives next to the
5//! history database at `<config-dir>/netrunner/settings.json`.
6
7use serde::{Deserialize, Serialize};
8use std::path::PathBuf;
9
10use crate::types::{DetailLevel, TestConfig};
11
12/// Persistent user preferences shared across runs.
13///
14/// `#[serde(default)]` makes the on-disk format forward/backward compatible:
15/// unknown fields are ignored and missing fields fall back to [`Default`].
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
17#[serde(default)]
18pub struct Settings {
19    /// Preferred test server URL.
20    pub server_url: String,
21    /// Test payload size in megabytes.
22    pub test_size_mb: u64,
23    /// Per-test timeout in seconds.
24    pub timeout_seconds: u64,
25    /// Output detail level.
26    pub detail_level: DetailLevel,
27    /// Whether animations/live charts are enabled.
28    pub animations: bool,
29    /// Automatically run a test when the app launches.
30    pub auto_run: bool,
31    /// How many past runs to show in history views.
32    pub max_history: usize,
33}
34
35impl Default for Settings {
36    fn default() -> Self {
37        let cfg = TestConfig::default();
38        Self {
39            server_url: cfg.server_url,
40            test_size_mb: cfg.test_size_mb,
41            timeout_seconds: cfg.timeout_seconds,
42            detail_level: cfg.detail_level,
43            animations: cfg.animation_enabled,
44            auto_run: true,
45            max_history: 20,
46        }
47    }
48}
49
50impl Settings {
51    /// Path to the settings file, creating the parent directory if needed.
52    pub fn config_path() -> Result<PathBuf, Box<dyn std::error::Error>> {
53        let dir = dirs::config_dir()
54            .ok_or("Failed to find config directory")?
55            .join("netrunner");
56        std::fs::create_dir_all(&dir)?;
57        Ok(dir.join("settings.json"))
58    }
59
60    /// Load settings from disk, falling back to defaults on any error
61    /// (missing file, partial JSON, parse failure).
62    pub fn load() -> Self {
63        Self::config_path()
64            .ok()
65            .and_then(|p| std::fs::read_to_string(p).ok())
66            .and_then(|s| serde_json::from_str(&s).ok())
67            .unwrap_or_default()
68    }
69
70    /// Persist settings to disk as pretty-printed JSON.
71    pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
72        let path = Self::config_path()?;
73        std::fs::write(path, serde_json::to_string_pretty(self)?)?;
74        Ok(())
75    }
76
77    /// Build a [`TestConfig`] from these settings.
78    pub fn to_config(&self) -> TestConfig {
79        TestConfig {
80            server_url: self.server_url.clone(),
81            test_size_mb: self.test_size_mb,
82            timeout_seconds: self.timeout_seconds,
83            json_output: false,
84            animation_enabled: self.animations,
85            detail_level: self.detail_level,
86            max_servers: 3,
87        }
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn default_matches_test_config() {
97        let s = Settings::default();
98        let c = s.to_config();
99        assert_eq!(c.server_url, TestConfig::default().server_url);
100        assert_eq!(c.test_size_mb, 10);
101        assert!(s.auto_run);
102        assert_eq!(s.max_history, 20);
103    }
104
105    #[test]
106    fn json_roundtrip() {
107        let s = Settings {
108            timeout_seconds: 42,
109            auto_run: false,
110            ..Default::default()
111        };
112        let json = serde_json::to_string_pretty(&s).unwrap();
113        let back: Settings = serde_json::from_str(&json).unwrap();
114        assert_eq!(s, back);
115    }
116
117    #[test]
118    fn partial_json_uses_defaults() {
119        // Only one field present — the rest must fall back to defaults.
120        let back: Settings = serde_json::from_str(r#"{ "timeout_seconds": 99 }"#).unwrap();
121        assert_eq!(back.timeout_seconds, 99);
122        assert_eq!(back.test_size_mb, Settings::default().test_size_mb);
123        assert!(back.auto_run);
124    }
125}