oxur_cli/config/
mod.rs

1//! Configuration module for the Oxur REPL CLI
2//!
3//! Provides a layered configuration system with support for:
4//! - Default values
5//! - TOML config file (~/.config/oxur/repl.toml)
6//! - Environment variables (OXUR_REPL_*)
7//! - CLI argument overrides
8//!
9//! # Example
10//!
11//! ```no_run
12//! use oxur_cli::config::{ReplConfig, TerminalConfig};
13//!
14//! // Load with all layers
15//! let config = ReplConfig::load(false).unwrap();
16//!
17//! // Or build programmatically
18//! let config = TerminalConfig::builder()
19//!     .prompt("λ> ")
20//!     .color(true)
21//!     .build();
22//! ```
23
24mod history;
25mod loader;
26mod metrics;
27pub mod paths;
28mod terminal;
29
30pub use history::{HistoryConfig, HistoryConfigBuilder};
31pub use loader::{load_config, ConfigLoader};
32pub use metrics::{MetricsConfig, MetricsConfigBuilder};
33pub use terminal::{EditMode, TerminalConfig, TerminalConfigBuilder};
34
35use serde::{Deserialize, Serialize};
36
37/// Top-level REPL configuration
38///
39/// Contains all configuration sections for the REPL.
40#[derive(Debug, Clone, Serialize, Deserialize, Default)]
41#[serde(default)]
42pub struct ReplConfig {
43    /// Terminal appearance and behavior
44    pub terminal: TerminalConfig,
45    /// Command history settings
46    pub history: HistoryConfig,
47    /// Metrics collection and export settings
48    pub metrics: MetricsConfig,
49}
50
51impl ReplConfig {
52    /// Load configuration with all layers applied
53    ///
54    /// Loads configuration in order of precedence:
55    /// 1. Defaults (lowest)
56    /// 2. Config file (if exists)
57    /// 3. Environment variables
58    /// 4. CLI arguments (highest)
59    ///
60    /// # Arguments
61    ///
62    /// * `no_color` - CLI flag to disable colors
63    ///
64    /// # Errors
65    ///
66    /// Returns error if config file exists but cannot be read or parsed.
67    pub fn load(no_color: bool) -> anyhow::Result<Self> {
68        load_config(no_color)
69    }
70
71    /// Create a new builder
72    pub fn builder() -> ReplConfigBuilder {
73        ReplConfigBuilder::new()
74    }
75
76    /// Merge another config into this one
77    ///
78    /// Values from `other` override values in `self`.
79    pub fn merge(&mut self, other: ReplConfig) {
80        self.terminal.merge(other.terminal);
81        self.history.merge(other.history);
82        self.metrics.merge(other.metrics);
83    }
84}
85
86/// Builder for ReplConfig
87#[derive(Debug, Clone, Default)]
88pub struct ReplConfigBuilder {
89    config: ReplConfig,
90}
91
92impl ReplConfigBuilder {
93    /// Create a new builder with defaults
94    pub fn new() -> Self {
95        Self { config: ReplConfig::default() }
96    }
97
98    /// Set terminal configuration
99    pub fn terminal(mut self, config: TerminalConfig) -> Self {
100        self.config.terminal = config;
101        self
102    }
103
104    /// Set history configuration
105    pub fn history(mut self, config: HistoryConfig) -> Self {
106        self.config.history = config;
107        self
108    }
109
110    /// Set metrics configuration
111    pub fn metrics(mut self, config: MetricsConfig) -> Self {
112        self.config.metrics = config;
113        self
114    }
115
116    /// Build the ReplConfig
117    pub fn build(self) -> ReplConfig {
118        self.config
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn test_repl_config_default() {
128        let config = ReplConfig::default();
129        assert_eq!(config.terminal.prompt, "oxur> ");
130        assert!(config.history.enabled);
131    }
132
133    #[test]
134    fn test_repl_config_builder() {
135        let terminal = TerminalConfig::builder().prompt(">>> ").build();
136
137        let history = HistoryConfig::builder().max_size(500).build();
138
139        let config = ReplConfig::builder().terminal(terminal).history(history).build();
140
141        assert_eq!(config.terminal.prompt, ">>> ");
142        assert_eq!(config.history.max_size, Some(500));
143    }
144
145    #[test]
146    fn test_serde_roundtrip() {
147        let config = ReplConfig::builder()
148            .terminal(TerminalConfig::builder().prompt("test> ").build())
149            .build();
150
151        let toml = toml::to_string(&config).unwrap();
152        let parsed: ReplConfig = toml::from_str(&toml).unwrap();
153
154        assert_eq!(config.terminal.prompt, parsed.terminal.prompt);
155    }
156
157    #[test]
158    fn test_merge() {
159        let mut base = ReplConfig::default();
160        let other = ReplConfig::builder()
161            .terminal(TerminalConfig::builder().prompt("merged> ").build())
162            .build();
163
164        base.merge(other);
165        assert_eq!(base.terminal.prompt, "merged> ");
166    }
167
168    #[test]
169    fn test_toml_parsing() {
170        let toml_str = r#"
171[terminal]
172prompt = "λ> "
173continuation_prompt = "  | "
174color_enabled = true
175edit_mode = "vi"
176
177[history]
178enabled = true
179max_size = 5000
180"#;
181
182        let config: ReplConfig = toml::from_str(toml_str).unwrap();
183        assert_eq!(config.terminal.prompt, "λ> ");
184        assert_eq!(config.terminal.continuation_prompt, "  | ");
185        assert_eq!(config.terminal.edit_mode, EditMode::Vi);
186        assert_eq!(config.history.max_size, Some(5000));
187    }
188}