subx_cli/config/
validation.rs

1//! Low-level validation functions for individual configuration values.
2//!
3//! This module provides primitive validation functions that are used by
4//! higher-level validation systems. These functions focus on validating
5//! individual values and types without knowledge of the overall configuration
6//! structure.
7//!
8//! # Architecture
9//!
10//! This module is the foundation of the validation system:
11//! - [`crate::config::validation`] (this module) - Low-level validation functions
12//! - [`crate::config::validator`] - High-level configuration section validators
13//! - [`crate::config::field_validator`] - Key-value validation for configuration service
14
15use crate::error::{SubXError, SubXResult};
16use std::path::Path;
17use url::Url;
18
19/// Validate a string value against a list of allowed values.
20pub fn validate_enum(value: &str, allowed: &[&str]) -> SubXResult<()> {
21    if allowed.contains(&value) {
22        Ok(())
23    } else {
24        Err(SubXError::config(format!(
25            "Invalid value '{}'. Allowed values: {}",
26            value,
27            allowed.join(", ")
28        )))
29    }
30}
31
32/// Validate a float value within a specified range.
33pub fn validate_float_range(value: &str, min: f32, max: f32) -> SubXResult<f32> {
34    let parsed = value
35        .parse::<f32>()
36        .map_err(|_| SubXError::config(format!("Invalid float value: {}", value)))?;
37    if parsed < min || parsed > max {
38        return Err(SubXError::config(format!(
39            "Value {} is out of range [{}, {}]",
40            parsed, min, max
41        )));
42    }
43    Ok(parsed)
44}
45
46/// Validate an unsigned integer within a specified range.
47pub fn validate_uint_range(value: &str, min: u32, max: u32) -> SubXResult<u32> {
48    let parsed = value
49        .parse::<u32>()
50        .map_err(|_| SubXError::config(format!("Invalid integer value: {}", value)))?;
51    if parsed < min || parsed > max {
52        return Err(SubXError::config(format!(
53            "Value {} is out of range [{}, {}]",
54            parsed, min, max
55        )));
56    }
57    Ok(parsed)
58}
59
60/// Validate a u64 value within a specified range.
61pub fn validate_u64_range(value: &str, min: u64, max: u64) -> SubXResult<u64> {
62    let parsed = value
63        .parse::<u64>()
64        .map_err(|_| SubXError::config(format!("Invalid u64 value: {}", value)))?;
65    if parsed < min || parsed > max {
66        return Err(SubXError::config(format!(
67            "Value {} is out of range [{}, {}]",
68            parsed, min, max
69        )));
70    }
71    Ok(parsed)
72}
73
74/// Validate a usize value within a specified range.
75pub fn validate_usize_range(value: &str, min: usize, max: usize) -> SubXResult<usize> {
76    let parsed = value
77        .parse::<usize>()
78        .map_err(|_| SubXError::config(format!("Invalid usize value: {}", value)))?;
79    if parsed < min || parsed > max {
80        return Err(SubXError::config(format!(
81            "Value {} is out of range [{}, {}]",
82            parsed, min, max
83        )));
84    }
85    Ok(parsed)
86}
87
88/// Validate API key format.
89pub fn validate_api_key(value: &str) -> SubXResult<()> {
90    if value.is_empty() {
91        return Err(SubXError::config("API key cannot be empty".to_string()));
92    }
93    if value.len() < 10 {
94        return Err(SubXError::config("API key is too short".to_string()));
95    }
96    Ok(())
97}
98
99/// Validate URL format.
100pub fn validate_url(value: &str) -> SubXResult<()> {
101    if !value.starts_with("http://") && !value.starts_with("https://") {
102        return Err(SubXError::config(format!(
103            "Invalid URL format: {}. Must start with http:// or https://",
104            value
105        )));
106    }
107    Ok(())
108}
109
110/// Parse boolean value from string.
111pub fn parse_bool(value: &str) -> SubXResult<bool> {
112    match value.to_lowercase().as_str() {
113        "true" | "1" | "yes" | "on" | "enabled" => Ok(true),
114        "false" | "0" | "no" | "off" | "disabled" => Ok(false),
115        _ => Err(SubXError::config(format!(
116            "Invalid boolean value: {}",
117            value
118        ))),
119    }
120}
121
122/// Validate that a string is a valid URL.
123///
124/// # Arguments
125/// * `value` - The string to validate as URL
126///
127/// # Errors
128/// Returns error if the string is not a valid URL format.
129pub fn validate_url_format(value: &str) -> SubXResult<()> {
130    if value.trim().is_empty() {
131        return Ok(()); // Empty URLs are often optional
132    }
133
134    Url::parse(value).map_err(|_| SubXError::config(format!("Invalid URL format: {}", value)))?;
135    Ok(())
136}
137
138/// Validate that a number is positive.
139///
140/// # Arguments
141/// * `value` - The number to validate
142///
143/// # Errors
144/// Returns error if the number is not positive.
145pub fn validate_positive_number<T>(value: T) -> SubXResult<()>
146where
147    T: PartialOrd + Default + std::fmt::Display + Copy,
148{
149    if value <= T::default() {
150        return Err(SubXError::config(format!(
151            "Value must be positive, got: {}",
152            value
153        )));
154    }
155    Ok(())
156}
157
158/// Validate that a number is within a specified range.
159///
160/// # Arguments
161/// * `value` - The value to validate
162/// * `min` - Minimum allowed value (inclusive)
163/// * `max` - Maximum allowed value (inclusive)
164///
165/// # Errors
166/// Returns error if the value is outside the specified range.
167pub fn validate_range<T>(value: T, min: T, max: T) -> SubXResult<()>
168where
169    T: PartialOrd + std::fmt::Display + Copy,
170{
171    if value < min || value > max {
172        return Err(SubXError::config(format!(
173            "Value {} is outside allowed range [{}, {}]",
174            value, min, max
175        )));
176    }
177    Ok(())
178}
179
180/// Validate that a string is not empty after trimming.
181///
182/// # Arguments
183/// * `value` - The string to validate
184/// * `field_name` - Name of the field for error messages
185///
186/// # Errors
187/// Returns error if the string is empty or contains only whitespace.
188pub fn validate_non_empty_string(value: &str, field_name: &str) -> SubXResult<()> {
189    if value.trim().is_empty() {
190        return Err(SubXError::config(format!("{} cannot be empty", field_name)));
191    }
192    Ok(())
193}
194
195/// Validate that a path exists and is accessible.
196///
197/// # Arguments
198/// * `value` - The path string to validate
199/// * `must_exist` - Whether the path must already exist
200///
201/// # Errors
202/// Returns error if path is invalid or doesn't exist when required.
203pub fn validate_file_path(value: &str, must_exist: bool) -> SubXResult<()> {
204    if value.trim().is_empty() {
205        return Err(SubXError::config("File path cannot be empty"));
206    }
207
208    let path = Path::new(value);
209    if must_exist && !path.exists() {
210        return Err(SubXError::config(format!("Path does not exist: {}", value)));
211    }
212
213    Ok(())
214}
215
216/// Validate temperature value for AI models.
217///
218/// # Arguments
219/// * `temperature` - The temperature value to validate
220///
221/// # Errors
222/// Returns error if temperature is outside the valid range (0.0-2.0).
223pub fn validate_temperature(temperature: f32) -> SubXResult<()> {
224    validate_range(temperature, 0.0, 2.0)
225        .map_err(|_| SubXError::config("AI temperature must be between 0.0 and 2.0"))
226}
227
228/// Validate AI model name format.
229///
230/// # Arguments
231/// * `model` - The model name to validate
232///
233/// # Errors
234/// Returns error if model name is invalid.
235pub fn validate_ai_model(model: &str) -> SubXResult<()> {
236    validate_non_empty_string(model, "AI model")?;
237
238    // Basic format validation - could be extended
239    if model.len() > 100 {
240        return Err(SubXError::config(
241            "AI model name is too long (max 100 characters)",
242        ));
243    }
244
245    Ok(())
246}
247
248/// Validate that a value is a power of two.
249///
250/// # Arguments
251/// * `value` - The value to check
252///
253/// # Errors
254/// Returns error if the value is not a power of two.
255pub fn validate_power_of_two(value: usize) -> SubXResult<()> {
256    if value == 0 || !value.is_power_of_two() {
257        return Err(SubXError::config(format!(
258            "Value {} must be a power of two",
259            value
260        )));
261    }
262    Ok(())
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268
269    #[test]
270    fn test_validate_url_format() {
271        assert!(validate_url_format("https://api.openai.com").is_ok());
272        assert!(validate_url_format("").is_ok()); // Empty is OK for optional fields
273        assert!(validate_url_format("invalid-url").is_err());
274        assert!(validate_url_format("ftp://example.com").is_ok()); // Different protocol is OK
275    }
276
277    #[test]
278    fn test_validate_positive_number() {
279        assert!(validate_positive_number(1.0).is_ok());
280        assert!(validate_positive_number(0.0).is_err());
281        assert!(validate_positive_number(-1.0).is_err());
282        assert!(validate_positive_number(0.1).is_ok());
283    }
284
285    #[test]
286    fn test_validate_range() {
287        assert!(validate_range(1.5, 0.0, 2.0).is_ok());
288        assert!(validate_range(0.0, 0.0, 2.0).is_ok()); // Boundary values OK
289        assert!(validate_range(2.0, 0.0, 2.0).is_ok());
290        assert!(validate_range(-0.1, 0.0, 2.0).is_err());
291        assert!(validate_range(2.1, 0.0, 2.0).is_err());
292    }
293
294    #[test]
295    fn test_validate_non_empty_string() {
296        assert!(validate_non_empty_string("test", "field").is_ok());
297        assert!(validate_non_empty_string("", "field").is_err());
298        assert!(validate_non_empty_string("   ", "field").is_err()); // Whitespace only
299        assert!(validate_non_empty_string(" test ", "field").is_ok());
300    }
301
302    #[test]
303    fn test_validate_temperature() {
304        assert!(validate_temperature(0.8).is_ok());
305        assert!(validate_temperature(0.0).is_ok());
306        assert!(validate_temperature(2.0).is_ok());
307        assert!(validate_temperature(-0.1).is_err());
308        assert!(validate_temperature(2.1).is_err());
309    }
310
311    #[test]
312    fn test_validate_ai_model() {
313        assert!(validate_ai_model("gpt-4").is_ok());
314        assert!(validate_ai_model("").is_err());
315        assert!(validate_ai_model(&"a".repeat(101)).is_err()); // Too long
316        assert!(validate_ai_model(&"a".repeat(100)).is_ok()); // Max length OK
317    }
318
319    #[test]
320    fn test_validate_power_of_two() {
321        assert!(validate_power_of_two(1).is_ok());
322        assert!(validate_power_of_two(2).is_ok());
323        assert!(validate_power_of_two(4).is_ok());
324        assert!(validate_power_of_two(256).is_ok());
325        assert!(validate_power_of_two(1024).is_ok());
326        assert!(validate_power_of_two(0).is_err());
327        assert!(validate_power_of_two(3).is_err());
328        assert!(validate_power_of_two(5).is_err());
329    }
330
331    #[test]
332    fn test_validate_enum() {
333        let allowed = &["openai", "anthropic"];
334        assert!(validate_enum("openai", allowed).is_ok());
335        assert!(validate_enum("anthropic", allowed).is_ok());
336        assert!(validate_enum("invalid", allowed).is_err());
337    }
338
339    #[test]
340    fn test_validate_float_range() {
341        assert!(validate_float_range("1.5", 0.0, 2.0).is_ok());
342        assert!(validate_float_range("0.0", 0.0, 2.0).is_ok());
343        assert!(validate_float_range("2.0", 0.0, 2.0).is_ok());
344        assert!(validate_float_range("-0.1", 0.0, 2.0).is_err());
345        assert!(validate_float_range("2.1", 0.0, 2.0).is_err());
346        assert!(validate_float_range("invalid", 0.0, 2.0).is_err());
347    }
348
349    #[test]
350    fn test_parse_bool() {
351        assert_eq!(parse_bool("true").unwrap(), true);
352        assert_eq!(parse_bool("false").unwrap(), false);
353        assert_eq!(parse_bool("1").unwrap(), true);
354        assert_eq!(parse_bool("0").unwrap(), false);
355        assert_eq!(parse_bool("yes").unwrap(), true);
356        assert_eq!(parse_bool("no").unwrap(), false);
357        assert_eq!(parse_bool("enabled").unwrap(), true);
358        assert_eq!(parse_bool("disabled").unwrap(), false);
359        assert!(parse_bool("invalid").is_err());
360    }
361}