Skip to main content

zod_rs/schema/
array.rs

1use crate::schema::Schema;
2use serde_json::Value;
3use std::fmt::Debug;
4use zod_rs_util::{
5    ValidateResult, ValidationError, ValidationOrigin, ValidationResult, ValidationType,
6};
7
8#[derive(Debug, Clone)]
9pub struct ArraySchema<S, T> {
10    element_schema: S,
11    min_length: Option<usize>,
12    max_length: Option<usize>,
13    _phantom: std::marker::PhantomData<T>,
14}
15
16impl<S, T> ArraySchema<S, T> {
17    pub fn new(element_schema: S) -> Self {
18        Self {
19            element_schema,
20            min_length: None,
21            max_length: None,
22            _phantom: std::marker::PhantomData,
23        }
24    }
25
26    pub fn min(mut self, min: usize) -> Self {
27        self.min_length = Some(min);
28        self
29    }
30
31    pub fn max(mut self, max: usize) -> Self {
32        self.max_length = Some(max);
33        self
34    }
35
36    pub fn length(self, len: usize) -> Self {
37        self.min(len).max(len)
38    }
39}
40
41impl<S, T> Schema<Vec<T>> for ArraySchema<S, T>
42where
43    S: Schema<T>,
44    T: Debug,
45{
46    fn validate(&self, value: &Value) -> ValidateResult<Vec<T>> {
47        let array = match value.as_array() {
48            Some(arr) => arr,
49            None => {
50                return Err(ValidationError::invalid_type(
51                    ValidationType::Array,
52                    ValidationType::from(value),
53                )
54                .into());
55            }
56        };
57
58        if let Some(min) = self.min_length {
59            if array.len() < min {
60                return Err(ValidationError::too_small(
61                    ValidationOrigin::Array,
62                    min.to_string(),
63                    true,
64                )
65                .into());
66            }
67        }
68
69        if let Some(max) = self.max_length {
70            if array.len() > max {
71                return Err(ValidationError::too_big(
72                    ValidationOrigin::Array,
73                    max.to_string(),
74                    true,
75                )
76                .into());
77            }
78        }
79
80        let mut results = Vec::new();
81        let mut validation_result = ValidationResult::new();
82
83        for (index, item) in array.iter().enumerate() {
84            match self.element_schema.validate(item) {
85                Ok(validated_item) => results.push(validated_item),
86                Err(mut errors) => {
87                    errors.prefix_path(index.to_string());
88                    validation_result.merge(errors);
89                }
90            }
91        }
92
93        if validation_result.is_empty() {
94            Ok(results)
95        } else {
96            Err(validation_result)
97        }
98    }
99}
100
101pub fn array<S, T>(element_schema: S) -> ArraySchema<S, T> {
102    ArraySchema::new(element_schema)
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use crate::schema::{number, string};
109    use serde_json::json;
110
111    #[test]
112    fn test_array_validation() {
113        let schema = array(string()).min(1).max(3);
114
115        assert!(schema.validate(&json!(["hello", "world"])).is_ok());
116        assert!(schema.validate(&json!([])).is_err());
117        assert!(schema.validate(&json!(["a", "b", "c", "d"])).is_err());
118        assert!(schema.validate(&json!([1, 2, 3])).is_err());
119    }
120
121    // ==================== EDGE CASE TESTS ====================
122
123    // Empty Array
124    #[test]
125    fn test_empty_array_with_min_zero() {
126        let schema = array(string()).min(0);
127        assert!(schema.validate(&json!([])).is_ok());
128    }
129
130    #[test]
131    fn test_empty_array_with_min_one() {
132        let schema = array(string()).min(1);
133        assert!(schema.validate(&json!([])).is_err());
134    }
135
136    #[test]
137    fn test_empty_array_no_constraints() {
138        let schema = array(string());
139        assert!(schema.validate(&json!([])).is_ok());
140    }
141
142    // Boundary Conditions
143    #[test]
144    fn test_exactly_at_min_length() {
145        let schema = array(string()).min(3);
146        assert!(schema.validate(&json!(["a", "b", "c"])).is_ok());
147        assert!(schema.validate(&json!(["a", "b"])).is_err());
148    }
149
150    #[test]
151    fn test_exactly_at_max_length() {
152        let schema = array(string()).max(3);
153        assert!(schema.validate(&json!(["a", "b", "c"])).is_ok());
154        assert!(schema.validate(&json!(["a", "b", "c", "d"])).is_err());
155    }
156
157    #[test]
158    fn test_exact_length() {
159        let schema = array(string()).length(2);
160        assert!(schema.validate(&json!(["a", "b"])).is_ok());
161        assert!(schema.validate(&json!(["a"])).is_err());
162        assert!(schema.validate(&json!(["a", "b", "c"])).is_err());
163    }
164
165    // Error Path Tracking
166    #[test]
167    fn test_error_at_index_zero() {
168        let schema = array(string().min(3));
169        let result = schema.validate(&json!(["hi"])); // "hi" is too short
170        assert!(result.is_err());
171        let err = result.unwrap_err();
172        assert_eq!(err.issues[0].path, vec!["0"]);
173    }
174
175    #[test]
176    fn test_error_at_middle_index() {
177        let schema = array(string().min(3));
178        let result = schema.validate(&json!(["hello", "hi", "world"]));
179        assert!(result.is_err());
180        let err = result.unwrap_err();
181        assert_eq!(err.issues[0].path, vec!["1"]);
182    }
183
184    #[test]
185    fn test_multiple_errors_across_indices() {
186        let schema = array(string().min(3));
187        let result = schema.validate(&json!(["hi", "ok", "x"]));
188        assert!(result.is_err());
189        let err = result.unwrap_err();
190        assert_eq!(err.issues.len(), 3);
191        // Check that paths are correct
192        let paths: Vec<_> = err.issues.iter().map(|i| i.path[0].as_str()).collect();
193        assert!(paths.contains(&"0"));
194        assert!(paths.contains(&"1"));
195        assert!(paths.contains(&"2"));
196    }
197
198    #[test]
199    fn test_valid_elements_return_correct_values() {
200        let schema = array(string());
201        let result = schema.validate(&json!(["a", "b", "c"]));
202        assert!(result.is_ok());
203        let values = result.unwrap();
204        assert_eq!(values, vec!["a", "b", "c"]);
205    }
206
207    // Nested Arrays
208    #[test]
209    fn test_nested_array() {
210        let schema = array(array(string()));
211        let result = schema.validate(&json!([["a", "b"], ["c", "d"]]));
212        assert!(result.is_ok());
213    }
214
215    #[test]
216    fn test_nested_array_error_path() {
217        let schema = array(array(string().min(3)));
218        let result = schema.validate(&json!([["hello"], ["hi"]]));
219        assert!(result.is_err());
220        let err = result.unwrap_err();
221        // Error should be at path ["1"]["0"] for the "hi" string
222        assert_eq!(err.issues[0].path, vec!["1", "0"]);
223    }
224
225    #[test]
226    fn test_deeply_nested_array() {
227        let schema = array(array(array(string())));
228        let result = schema.validate(&json!([[["a", "b"]], [["c", "d"]]]));
229        assert!(result.is_ok());
230    }
231
232    // Mixed Element Types (should fail for wrong types)
233    #[test]
234    fn test_mixed_types_fail() {
235        let schema = array(string());
236        assert!(schema.validate(&json!(["hello", 123, "world"])).is_err());
237    }
238
239    #[test]
240    fn test_number_array() {
241        let schema = array(number().positive());
242        assert!(schema.validate(&json!([1, 2, 3])).is_ok());
243        assert!(schema.validate(&json!([1, -2, 3])).is_err());
244    }
245
246    // Constraint Conflicts
247    #[test]
248    fn test_impossible_constraint_min_greater_than_max() {
249        let schema = array(string()).min(5).max(2);
250        // All arrays will fail
251        assert!(schema.validate(&json!(["a", "b", "c"])).is_err());
252        assert!(schema.validate(&json!(["a"])).is_err());
253        assert!(schema.validate(&json!(["a", "b", "c", "d", "e", "f"])).is_err());
254    }
255
256    // Type Rejection
257    #[test]
258    fn test_rejects_null() {
259        let schema = array(string());
260        assert!(schema.validate(&json!(null)).is_err());
261    }
262
263    #[test]
264    fn test_rejects_string() {
265        let schema = array(string());
266        assert!(schema.validate(&json!("not an array")).is_err());
267    }
268
269    #[test]
270    fn test_rejects_number() {
271        let schema = array(string());
272        assert!(schema.validate(&json!(123)).is_err());
273    }
274
275    #[test]
276    fn test_rejects_object() {
277        let schema = array(string());
278        assert!(schema.validate(&json!({"key": "value"})).is_err());
279    }
280
281    #[test]
282    fn test_rejects_boolean() {
283        let schema = array(string());
284        assert!(schema.validate(&json!(true)).is_err());
285    }
286
287    // Single Element Array
288    #[test]
289    fn test_single_element_valid() {
290        let schema = array(string());
291        assert!(schema.validate(&json!(["single"])).is_ok());
292    }
293
294    #[test]
295    fn test_single_element_invalid() {
296        let schema = array(string().min(10));
297        let result = schema.validate(&json!(["short"]));
298        assert!(result.is_err());
299        let err = result.unwrap_err();
300        assert_eq!(err.issues[0].path, vec!["0"]);
301    }
302
303    // Large Array (performance sanity check)
304    #[test]
305    fn test_large_array() {
306        let schema = array(number());
307        let large_array: Vec<i32> = (0..1000).collect();
308        let json_val = serde_json::to_value(large_array).unwrap();
309        assert!(schema.validate(&json_val).is_ok());
310    }
311}