Skip to main content

sqry_core/config/
workspace.rs

1//! Workspace configuration for sqry.
2//!
3//! This module contains configuration for workspace discovery and resolution.
4
5use anyhow::{Result, bail};
6use serde::{Deserialize, Serialize};
7use std::env;
8
9/// Workspace configuration
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct WorkspaceConfig {
12    /// Maximum depth for workspace discovery (default: 100)
13    ///
14    /// Controls how far up the directory tree the workspace resolver will
15    /// search for a `.sqry` directory. This prevents infinite loops and
16    /// excessive filesystem traversal.
17    ///
18    /// # Validation
19    /// - Minimum: 10 (must search at least 10 levels)
20    /// - Maximum: 1000 (recommended limit)
21    /// - Hard cap: 1000 (absolute maximum, security constraint)
22    ///
23    /// # Environment Override
24    /// Can be overridden with `SQRY_WORKSPACE_DISCOVERY_DEPTH` environment variable.
25    pub discovery_max_depth: usize,
26}
27
28impl Default for WorkspaceConfig {
29    fn default() -> Self {
30        Self {
31            discovery_max_depth: 100,
32        }
33    }
34}
35
36impl WorkspaceConfig {
37    /// Minimum allowed discovery depth
38    const MIN_DISCOVERY_DEPTH: usize = 10;
39
40    /// Maximum recommended discovery depth
41    const MAX_DISCOVERY_DEPTH: usize = 1000;
42
43    /// Absolute hard cap for discovery depth (security constraint)
44    const ABSOLUTE_MAX_DISCOVERY_DEPTH: usize = 1000;
45
46    /// Create a new workspace configuration with custom discovery depth
47    ///
48    /// # Errors
49    ///
50    /// Returns an error if the provided discovery depth violates safety constraints.
51    pub fn new(discovery_max_depth: usize) -> Result<Self> {
52        let config = Self {
53            discovery_max_depth,
54        };
55        config.validate()?;
56        Ok(config)
57    }
58
59    /// Load configuration with environment variable overrides
60    ///
61    /// # Errors
62    ///
63    /// Returns an error if environment variables contain invalid values or if validation fails.
64    pub fn load_or_default() -> Result<Self> {
65        let mut config = Self::default();
66
67        // Apply environment variable override if present
68        if let Ok(depth_str) = env::var("SQRY_WORKSPACE_DISCOVERY_DEPTH") {
69            config.discovery_max_depth =
70                Self::parse_env_var(&depth_str, "SQRY_WORKSPACE_DISCOVERY_DEPTH")?;
71        }
72
73        config.validate()?;
74        Ok(config)
75    }
76
77    /// Get effective discovery depth with validation
78    ///
79    /// This method enforces all validation constraints and returns the
80    /// safe-to-use discovery depth value.
81    ///
82    /// # Errors
83    /// Returns an error if:
84    /// - Value is 0 (unlimited not allowed)
85    /// - Value is below minimum (< 10)
86    /// - Value exceeds hard cap (> 1000)
87    pub fn effective_discovery_depth(&self) -> Result<usize> {
88        if self.discovery_max_depth == 0 {
89            bail!("workspace.discovery_max_depth cannot be 0 (unlimited not allowed for safety)");
90        }
91
92        if self.discovery_max_depth < Self::MIN_DISCOVERY_DEPTH {
93            bail!(
94                "workspace.discovery_max_depth {} is below minimum {}",
95                self.discovery_max_depth,
96                Self::MIN_DISCOVERY_DEPTH
97            );
98        }
99
100        if self.discovery_max_depth > Self::MAX_DISCOVERY_DEPTH {
101            tracing::warn!(
102                "workspace.discovery_max_depth {} exceeds recommended maximum {}",
103                self.discovery_max_depth,
104                Self::MAX_DISCOVERY_DEPTH
105            );
106        }
107
108        if self.discovery_max_depth > Self::ABSOLUTE_MAX_DISCOVERY_DEPTH {
109            bail!(
110                "workspace.discovery_max_depth {} exceeds absolute hard cap {}",
111                self.discovery_max_depth,
112                Self::ABSOLUTE_MAX_DISCOVERY_DEPTH
113            );
114        }
115
116        Ok(self.discovery_max_depth)
117    }
118
119    /// Validate the configuration
120    fn validate(&self) -> Result<()> {
121        // Validation happens in effective_discovery_depth()
122        self.effective_discovery_depth()?;
123        Ok(())
124    }
125
126    /// Parse environment variable with strict error handling
127    ///
128    /// This method implements the fail-fast parse policy:
129    /// - Parse errors result in immediate failure with clear message
130    /// - Out-of-range values result in immediate failure
131    /// - NO silent fallbacks to default values
132    fn parse_env_var(value: &str, var_name: &str) -> Result<usize> {
133        match value.parse::<usize>() {
134            Ok(parsed) => Ok(parsed),
135            Err(_) => bail!(
136                "Invalid value for {}: '{}'. Expected usize in range [{}, {}]",
137                var_name,
138                value,
139                Self::MIN_DISCOVERY_DEPTH,
140                Self::ABSOLUTE_MAX_DISCOVERY_DEPTH
141            ),
142        }
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_default_config() {
152        let config = WorkspaceConfig::default();
153        assert_eq!(config.discovery_max_depth, 100);
154        assert!(config.effective_discovery_depth().is_ok());
155    }
156
157    #[test]
158    fn test_new_with_valid_depth() {
159        let config = WorkspaceConfig::new(50).unwrap();
160        assert_eq!(config.effective_discovery_depth().unwrap(), 50);
161    }
162
163    #[test]
164    fn test_new_with_zero_depth_fails() {
165        let result = WorkspaceConfig::new(0);
166        assert!(result.is_err());
167        assert!(result.unwrap_err().to_string().contains("cannot be 0"));
168    }
169
170    #[test]
171    fn test_below_minimum_fails() {
172        let result = WorkspaceConfig::new(5);
173        assert!(result.is_err());
174        assert!(result.unwrap_err().to_string().contains("below minimum 10"));
175    }
176
177    #[test]
178    fn test_at_minimum_succeeds() {
179        let config = WorkspaceConfig::new(10).unwrap();
180        assert_eq!(config.effective_discovery_depth().unwrap(), 10);
181    }
182
183    #[test]
184    fn test_at_maximum_succeeds() {
185        let config = WorkspaceConfig::new(1000).unwrap();
186        assert_eq!(config.effective_discovery_depth().unwrap(), 1000);
187    }
188
189    #[test]
190    fn test_above_hard_cap_fails() {
191        let result = WorkspaceConfig::new(1001);
192        assert!(result.is_err());
193        assert!(
194            result
195                .unwrap_err()
196                .to_string()
197                .contains("exceeds absolute hard cap")
198        );
199    }
200
201    #[test]
202    fn test_parse_env_var_valid() {
203        let result = WorkspaceConfig::parse_env_var("50", "TEST_VAR");
204        assert_eq!(result.unwrap(), 50);
205    }
206
207    #[test]
208    fn test_parse_env_var_invalid() {
209        let result = WorkspaceConfig::parse_env_var("abc", "TEST_VAR");
210        assert!(result.is_err());
211        assert!(
212            result
213                .unwrap_err()
214                .to_string()
215                .contains("Invalid value for TEST_VAR")
216        );
217    }
218
219    #[test]
220    fn test_parse_env_var_negative() {
221        let result = WorkspaceConfig::parse_env_var("-10", "TEST_VAR");
222        assert!(result.is_err());
223        assert!(result.unwrap_err().to_string().contains("Invalid value"));
224    }
225
226    #[test]
227    fn test_effective_depth_warns_at_max() {
228        let config = WorkspaceConfig {
229            discovery_max_depth: 1000,
230        };
231        // Should succeed but log warning
232        assert_eq!(config.effective_discovery_depth().unwrap(), 1000);
233    }
234}