oxify_storage/
validation.rs

1//! Validation utilities for storage layer
2//!
3//! Provides reusable validation functions to ensure data consistency
4//! and reduce code duplication across store modules.
5
6use crate::{Result, StorageError};
7use std::fmt::Display;
8
9/// Validate that a value is positive
10///
11/// # Examples
12/// ```
13/// # use oxify_storage::validation::validate_positive;
14/// assert!(validate_positive(10, "count").is_ok());
15/// assert!(validate_positive(0, "count").is_err());
16/// assert!(validate_positive(-5, "count").is_err());
17/// ```
18pub fn validate_positive<T: PartialOrd + Default + Display>(
19    value: T,
20    field_name: &str,
21) -> Result<T> {
22    if value <= T::default() {
23        return Err(StorageError::validation(format!(
24            "{} must be positive, got {}",
25            field_name, value
26        )));
27    }
28    Ok(value)
29}
30
31/// Validate optional positive value
32///
33/// Returns Ok(None) if value is None, validates if Some
34pub fn validate_optional_positive<T: PartialOrd + Default + Display + Copy>(
35    value: Option<T>,
36    field_name: &str,
37) -> Result<Option<T>> {
38    if let Some(v) = value {
39        validate_positive(v, field_name)?;
40    }
41    Ok(value)
42}
43
44/// Validate string is not empty or whitespace
45///
46/// # Examples
47/// ```
48/// # use oxify_storage::validation::validate_non_empty_string;
49/// assert!(validate_non_empty_string("test", "field").is_ok());
50/// assert!(validate_non_empty_string("", "field").is_err());
51/// assert!(validate_non_empty_string("   ", "field").is_err());
52/// ```
53pub fn validate_non_empty_string(value: &str, field_name: &str) -> Result<()> {
54    if value.trim().is_empty() {
55        return Err(StorageError::validation(format!(
56            "{} cannot be empty",
57            field_name
58        )));
59    }
60    Ok(())
61}
62
63/// Validate collection size is within bounds
64///
65/// # Examples
66/// ```
67/// # use oxify_storage::validation::validate_collection_size;
68/// let items = vec![1, 2, 3];
69/// assert!(validate_collection_size(&items, 5, "items").is_ok());
70/// assert!(validate_collection_size(&items, 2, "items").is_err());
71/// ```
72pub fn validate_collection_size<T>(
73    collection: &[T],
74    max_size: usize,
75    collection_name: &str,
76) -> Result<()> {
77    if collection.len() > max_size {
78        return Err(StorageError::validation(format!(
79            "{} has {} items, which exceeds the maximum of {}",
80            collection_name,
81            collection.len(),
82            max_size
83        )));
84    }
85    Ok(())
86}
87
88/// Builder for quota limit validation
89///
90/// Accumulates validation errors and reports them all at once.
91///
92/// # Examples
93/// ```
94/// # use oxify_storage::validation::QuotaLimitValidator;
95/// let result = QuotaLimitValidator::new()
96///     .validate_limit(Some(100), "max_executions")
97///     .validate_limit(Some(50), "max_tokens")
98///     .build();
99/// assert!(result.is_ok());
100///
101/// let result = QuotaLimitValidator::new()
102///     .validate_limit(Some(-1), "max_executions")
103///     .validate_limit(Some(0), "max_tokens")
104///     .build();
105/// assert!(result.is_err());
106/// ```
107pub struct QuotaLimitValidator {
108    errors: Vec<String>,
109}
110
111impl QuotaLimitValidator {
112    /// Create a new validator
113    pub fn new() -> Self {
114        Self { errors: Vec::new() }
115    }
116
117    /// Validate an optional i32 limit value
118    pub fn validate_limit(mut self, value: Option<i32>, name: &str) -> Self {
119        if let Some(limit) = value {
120            if limit <= 0 {
121                self.errors
122                    .push(format!("{} must be positive, got {}", name, limit));
123            }
124        }
125        self
126    }
127
128    /// Validate an optional i64 limit value
129    pub fn validate_limit_i64(mut self, value: Option<i64>, name: &str) -> Self {
130        if let Some(limit) = value {
131            if limit <= 0 {
132                self.errors
133                    .push(format!("{} must be positive, got {}", name, limit));
134            }
135        }
136        self
137    }
138
139    /// Build the validation result
140    ///
141    /// Returns Ok if no errors, or Err with all accumulated errors
142    pub fn build(self) -> Result<()> {
143        if self.errors.is_empty() {
144            Ok(())
145        } else {
146            Err(StorageError::validation(self.errors.join("; ")))
147        }
148    }
149}
150
151impl Default for QuotaLimitValidator {
152    fn default() -> Self {
153        Self::new()
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn test_validate_positive_i32() {
163        assert!(validate_positive(10, "test").is_ok());
164        assert!(validate_positive(1, "test").is_ok());
165
166        let result = validate_positive(0, "test");
167        assert!(result.is_err());
168
169        let result = validate_positive(-5, "test");
170        assert!(result.is_err());
171    }
172
173    #[test]
174    fn test_validate_positive_i64() {
175        assert!(validate_positive(100i64, "test").is_ok());
176        assert!(validate_positive(0i64, "test").is_err());
177        assert!(validate_positive(-10i64, "test").is_err());
178    }
179
180    #[test]
181    fn test_validate_optional_positive() {
182        assert!(validate_optional_positive(Some(10), "test").is_ok());
183        assert!(validate_optional_positive(None::<i32>, "test").is_ok());
184        assert!(validate_optional_positive(Some(0), "test").is_err());
185        assert!(validate_optional_positive(Some(-5), "test").is_err());
186    }
187
188    #[test]
189    fn test_validate_non_empty_string() {
190        assert!(validate_non_empty_string("test", "field").is_ok());
191        assert!(validate_non_empty_string("a", "field").is_ok());
192
193        let result = validate_non_empty_string("", "field");
194        assert!(result.is_err());
195        assert!(result.unwrap_err().to_string().contains("cannot be empty"));
196
197        let result = validate_non_empty_string("   ", "field");
198        assert!(result.is_err());
199    }
200
201    #[test]
202    fn test_validate_collection_size() {
203        let items = vec![1, 2, 3];
204        assert!(validate_collection_size(&items, 5, "items").is_ok());
205        assert!(validate_collection_size(&items, 3, "items").is_ok());
206
207        let result = validate_collection_size(&items, 2, "items");
208        assert!(result.is_err());
209        assert!(result
210            .unwrap_err()
211            .to_string()
212            .contains("exceeds the maximum"));
213    }
214
215    #[test]
216    fn test_quota_limit_validator_valid() {
217        let result = QuotaLimitValidator::new()
218            .validate_limit(Some(100), "max_executions")
219            .validate_limit(Some(50), "max_tokens")
220            .validate_limit_i64(Some(1000), "max_cost")
221            .build();
222        assert!(result.is_ok());
223    }
224
225    #[test]
226    fn test_quota_limit_validator_with_none() {
227        let result = QuotaLimitValidator::new()
228            .validate_limit(None, "max_executions")
229            .validate_limit(Some(50), "max_tokens")
230            .build();
231        assert!(result.is_ok());
232    }
233
234    #[test]
235    fn test_quota_limit_validator_invalid() {
236        let result = QuotaLimitValidator::new()
237            .validate_limit(Some(-1), "max_executions")
238            .validate_limit(Some(0), "max_tokens")
239            .build();
240
241        assert!(result.is_err());
242        let error_msg = result.unwrap_err().to_string();
243        assert!(error_msg.contains("max_executions"));
244        assert!(error_msg.contains("max_tokens"));
245    }
246
247    #[test]
248    fn test_quota_limit_validator_multiple_errors() {
249        let result = QuotaLimitValidator::new()
250            .validate_limit(Some(-1), "field1")
251            .validate_limit(Some(0), "field2")
252            .validate_limit_i64(Some(-100), "field3")
253            .build();
254
255        assert!(result.is_err());
256        let error_msg = result.unwrap_err().to_string();
257        assert!(error_msg.contains("field1"));
258        assert!(error_msg.contains("field2"));
259        assert!(error_msg.contains("field3"));
260    }
261}