subx_cli/config/
validation.rs1use crate::error::{SubXError, SubXResult};
16use std::path::Path;
17use url::Url;
18
19pub 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
32pub 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 {parsed} is out of range [{min}, {max}]"
40 )));
41 }
42 Ok(parsed)
43}
44
45pub fn validate_uint_range(value: &str, min: u32, max: u32) -> SubXResult<u32> {
47 let parsed = value
48 .parse::<u32>()
49 .map_err(|_| SubXError::config(format!("Invalid integer value: {value}")))?;
50 if parsed < min || parsed > max {
51 return Err(SubXError::config(format!(
52 "Value {parsed} is out of range [{min}, {max}]"
53 )));
54 }
55 Ok(parsed)
56}
57
58pub fn validate_u64_range(value: &str, min: u64, max: u64) -> SubXResult<u64> {
60 let parsed = value
61 .parse::<u64>()
62 .map_err(|_| SubXError::config(format!("Invalid u64 value: {value}")))?;
63 if parsed < min || parsed > max {
64 return Err(SubXError::config(format!(
65 "Value {parsed} is out of range [{min}, {max}]"
66 )));
67 }
68 Ok(parsed)
69}
70
71pub fn validate_usize_range(value: &str, min: usize, max: usize) -> SubXResult<usize> {
73 let parsed = value
74 .parse::<usize>()
75 .map_err(|_| SubXError::config(format!("Invalid usize value: {value}")))?;
76 if parsed < min || parsed > max {
77 return Err(SubXError::config(format!(
78 "Value {parsed} is out of range [{min}, {max}]"
79 )));
80 }
81 Ok(parsed)
82}
83
84pub fn validate_api_key(value: &str) -> SubXResult<()> {
86 if value.is_empty() {
87 return Err(SubXError::config("API key cannot be empty".to_string()));
88 }
89 if value.len() < 10 {
90 return Err(SubXError::config("API key is too short".to_string()));
91 }
92 Ok(())
93}
94
95pub fn validate_url(value: &str) -> SubXResult<()> {
97 if !value.starts_with("http://") && !value.starts_with("https://") {
98 return Err(SubXError::config(format!(
99 "Invalid URL format: {value}. Must start with http:// or https://"
100 )));
101 }
102 Ok(())
103}
104
105pub fn parse_bool(value: &str) -> SubXResult<bool> {
107 match value.to_lowercase().as_str() {
108 "true" | "1" | "yes" | "on" | "enabled" => Ok(true),
109 "false" | "0" | "no" | "off" | "disabled" => Ok(false),
110 _ => Err(SubXError::config(format!("Invalid boolean value: {value}"))),
111 }
112}
113
114pub fn validate_url_format(value: &str) -> SubXResult<()> {
122 if value.trim().is_empty() {
123 return Ok(()); }
125
126 Url::parse(value).map_err(|_| SubXError::config(format!("Invalid URL format: {value}")))?;
127 Ok(())
128}
129
130pub fn validate_positive_number<T>(value: T) -> SubXResult<()>
138where
139 T: PartialOrd + Default + std::fmt::Display + Copy,
140{
141 if value <= T::default() {
142 return Err(SubXError::config(format!(
143 "Value must be positive, got: {value}"
144 )));
145 }
146 Ok(())
147}
148
149pub fn validate_range<T>(value: T, min: T, max: T) -> SubXResult<()>
159where
160 T: PartialOrd + std::fmt::Display + Copy,
161{
162 if value < min || value > max {
163 return Err(SubXError::config(format!(
164 "Value {value} is outside allowed range [{min}, {max}]"
165 )));
166 }
167 Ok(())
168}
169
170pub fn validate_non_empty_string(value: &str, field_name: &str) -> SubXResult<()> {
179 if value.trim().is_empty() {
180 return Err(SubXError::config(format!("{field_name} cannot be empty")));
181 }
182 Ok(())
183}
184
185pub fn validate_file_path(value: &str, must_exist: bool) -> SubXResult<()> {
194 if value.trim().is_empty() {
195 return Err(SubXError::config("File path cannot be empty"));
196 }
197
198 let path = Path::new(value);
199 if must_exist && !path.exists() {
200 return Err(SubXError::config(format!("Path does not exist: {value}")));
201 }
202
203 Ok(())
204}
205
206pub fn validate_temperature(temperature: f32) -> SubXResult<()> {
214 validate_range(temperature, 0.0, 2.0)
215 .map_err(|_| SubXError::config("AI temperature must be between 0.0 and 2.0"))
216}
217
218pub fn validate_ai_model(model: &str) -> SubXResult<()> {
226 validate_non_empty_string(model, "AI model")?;
227
228 if model.len() > 100 {
230 return Err(SubXError::config(
231 "AI model name is too long (max 100 characters)",
232 ));
233 }
234
235 Ok(())
236}
237
238pub fn validate_power_of_two(value: usize) -> SubXResult<()> {
246 if value == 0 || !value.is_power_of_two() {
247 return Err(SubXError::config(format!(
248 "Value {value} must be a power of two"
249 )));
250 }
251 Ok(())
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn test_validate_url_format() {
260 assert!(validate_url_format("https://api.openai.com").is_ok());
261 assert!(validate_url_format("").is_ok()); assert!(validate_url_format("invalid-url").is_err());
263 assert!(validate_url_format("ftp://example.com").is_ok()); }
265
266 #[test]
267 fn test_validate_positive_number() {
268 assert!(validate_positive_number(1.0).is_ok());
269 assert!(validate_positive_number(0.0).is_err());
270 assert!(validate_positive_number(-1.0).is_err());
271 assert!(validate_positive_number(0.1).is_ok());
272 }
273
274 #[test]
275 fn test_validate_range() {
276 assert!(validate_range(1.5, 0.0, 2.0).is_ok());
277 assert!(validate_range(0.0, 0.0, 2.0).is_ok()); assert!(validate_range(2.0, 0.0, 2.0).is_ok());
279 assert!(validate_range(-0.1, 0.0, 2.0).is_err());
280 assert!(validate_range(2.1, 0.0, 2.0).is_err());
281 }
282
283 #[test]
284 fn test_validate_non_empty_string() {
285 assert!(validate_non_empty_string("test", "field").is_ok());
286 assert!(validate_non_empty_string("", "field").is_err());
287 assert!(validate_non_empty_string(" ", "field").is_err()); assert!(validate_non_empty_string(" test ", "field").is_ok());
289 }
290
291 #[test]
292 fn test_validate_temperature() {
293 assert!(validate_temperature(0.8).is_ok());
294 assert!(validate_temperature(0.0).is_ok());
295 assert!(validate_temperature(2.0).is_ok());
296 assert!(validate_temperature(-0.1).is_err());
297 assert!(validate_temperature(2.1).is_err());
298 }
299
300 #[test]
301 fn test_validate_ai_model() {
302 assert!(validate_ai_model("gpt-4").is_ok());
303 assert!(validate_ai_model("").is_err());
304 assert!(validate_ai_model(&"a".repeat(101)).is_err()); assert!(validate_ai_model(&"a".repeat(100)).is_ok()); }
307
308 #[test]
309 fn test_validate_power_of_two() {
310 assert!(validate_power_of_two(1).is_ok());
311 assert!(validate_power_of_two(2).is_ok());
312 assert!(validate_power_of_two(4).is_ok());
313 assert!(validate_power_of_two(256).is_ok());
314 assert!(validate_power_of_two(1024).is_ok());
315 assert!(validate_power_of_two(0).is_err());
316 assert!(validate_power_of_two(3).is_err());
317 assert!(validate_power_of_two(5).is_err());
318 }
319
320 #[test]
321 fn test_validate_enum() {
322 let allowed = &["openai", "anthropic", "openrouter"];
323 assert!(validate_enum("openai", allowed).is_ok());
324 assert!(validate_enum("anthropic", allowed).is_ok());
325 assert!(validate_enum("openrouter", allowed).is_ok());
326 assert!(validate_enum("invalid", allowed).is_err());
327 }
328
329 #[test]
330 fn test_validate_float_range() {
331 assert!(validate_float_range("1.5", 0.0, 2.0).is_ok());
332 assert!(validate_float_range("0.0", 0.0, 2.0).is_ok());
333 assert!(validate_float_range("2.0", 0.0, 2.0).is_ok());
334 assert!(validate_float_range("-0.1", 0.0, 2.0).is_err());
335 assert!(validate_float_range("2.1", 0.0, 2.0).is_err());
336 assert!(validate_float_range("invalid", 0.0, 2.0).is_err());
337 }
338
339 #[test]
340 fn test_parse_bool() {
341 assert_eq!(parse_bool("true").unwrap(), true);
342 assert_eq!(parse_bool("false").unwrap(), false);
343 assert_eq!(parse_bool("1").unwrap(), true);
344 assert_eq!(parse_bool("0").unwrap(), false);
345 assert_eq!(parse_bool("yes").unwrap(), true);
346 assert_eq!(parse_bool("no").unwrap(), false);
347 assert_eq!(parse_bool("enabled").unwrap(), true);
348 assert_eq!(parse_bool("disabled").unwrap(), false);
349 assert!(parse_bool("invalid").is_err());
350 }
351}