Skip to main content

vld/collections/
array.rs

1use serde_json::Value;
2
3use crate::error::{value_type_name, IssueCode, PathSegment, VldError};
4use crate::schema::VldSchema;
5
6/// Schema for array validation. Created via [`vld::array()`](crate::array).
7///
8/// Validates each element using the provided element schema.
9///
10/// # Example
11/// ```
12/// use vld::prelude::*;
13///
14/// let schema = vld::array(vld::string().min(1)).min_len(1).max_len(10);
15/// ```
16pub struct ZArray<T: VldSchema> {
17    element: T,
18    min_len: Option<usize>,
19    max_len: Option<usize>,
20    exact_len: Option<usize>,
21    contains: Option<serde_json::Value>,
22    min_contains: Option<usize>,
23    max_contains: Option<usize>,
24    unique: bool,
25}
26
27impl<T: VldSchema> ZArray<T> {
28    pub fn new(element: T) -> Self {
29        Self {
30            element,
31            min_len: None,
32            max_len: None,
33            exact_len: None,
34            contains: None,
35            min_contains: None,
36            max_contains: None,
37            unique: false,
38        }
39    }
40
41    /// Minimum number of elements.
42    pub fn min_len(mut self, len: usize) -> Self {
43        self.min_len = Some(len);
44        self
45    }
46
47    /// Maximum number of elements.
48    pub fn max_len(mut self, len: usize) -> Self {
49        self.max_len = Some(len);
50        self
51    }
52
53    /// Exact number of elements.
54    pub fn len(mut self, len: usize) -> Self {
55        self.exact_len = Some(len);
56        self
57    }
58
59    /// Alias for `min_len(1)` — array must not be empty.
60    pub fn non_empty(self) -> Self {
61        self.min_len(1)
62    }
63
64    /// Require the array to contain this JSON value.
65    pub fn contains(mut self, value: impl Into<serde_json::Value>) -> Self {
66        self.contains = Some(value.into());
67        self
68    }
69
70    /// Require at least this many matches of the `contains(...)` value.
71    pub fn min_contains(mut self, n: usize) -> Self {
72        self.min_contains = Some(n);
73        self
74    }
75
76    /// Require at most this many matches of the `contains(...)` value.
77    pub fn max_contains(mut self, n: usize) -> Self {
78        self.max_contains = Some(n);
79        self
80    }
81
82    /// Require all array items to be unique (by raw JSON equality).
83    pub fn unique(mut self) -> Self {
84        self.unique = true;
85        self
86    }
87
88    #[allow(dead_code)]
89    pub(crate) fn element_schema(&self) -> &T {
90        &self.element
91    }
92
93    /// Generate a JSON Schema (called by [`JsonSchema`](crate::json_schema::JsonSchema) trait impl).
94    ///
95    /// Requires the `openapi` feature.
96    #[cfg(feature = "openapi")]
97    pub fn to_json_schema_inner(&self) -> serde_json::Value
98    where
99        T: crate::json_schema::JsonSchema,
100    {
101        let mut schema = serde_json::json!({
102            "type": "array",
103            "items": self.element.json_schema(),
104        });
105        if let Some(min) = self.min_len {
106            schema["minItems"] = serde_json::json!(min);
107        }
108        if let Some(max) = self.max_len {
109            schema["maxItems"] = serde_json::json!(max);
110        }
111        if let Some(exact) = self.exact_len {
112            schema["minItems"] = serde_json::json!(exact);
113            schema["maxItems"] = serde_json::json!(exact);
114        }
115        if let Some(ref contains) = self.contains {
116            schema["contains"] = contains.clone();
117        }
118        if let Some(min_contains) = self.min_contains {
119            schema["minContains"] = serde_json::json!(min_contains);
120        }
121        if let Some(max_contains) = self.max_contains {
122            schema["maxContains"] = serde_json::json!(max_contains);
123        }
124        if self.unique {
125            schema["uniqueItems"] = serde_json::json!(true);
126        }
127        schema
128    }
129}
130
131impl<T: VldSchema> VldSchema for ZArray<T> {
132    type Output = Vec<T::Output>;
133
134    fn parse_value(&self, value: &Value) -> Result<Vec<T::Output>, VldError> {
135        let arr = value.as_array().ok_or_else(|| {
136            VldError::single(
137                IssueCode::InvalidType {
138                    expected: "array".to_string(),
139                    received: value_type_name(value),
140                },
141                format!("Expected array, received {}", value_type_name(value)),
142            )
143        })?;
144
145        let mut errors = VldError::new();
146
147        // Length checks
148        if let Some(min) = self.min_len {
149            if arr.len() < min {
150                errors.push(
151                    IssueCode::TooSmall {
152                        minimum: min as f64,
153                        inclusive: true,
154                    },
155                    format!("Array must have at least {} elements", min),
156                );
157            }
158        }
159
160        if let Some(max) = self.max_len {
161            if arr.len() > max {
162                errors.push(
163                    IssueCode::TooBig {
164                        maximum: max as f64,
165                        inclusive: true,
166                    },
167                    format!("Array must have at most {} elements", max),
168                );
169            }
170        }
171
172        if let Some(exact) = self.exact_len {
173            if arr.len() != exact {
174                errors.push(
175                    IssueCode::Custom {
176                        code: "invalid_length".to_string(),
177                    },
178                    format!("Array must have exactly {} elements", exact),
179                );
180            }
181        }
182
183        if self.unique {
184            for i in 0..arr.len() {
185                for j in (i + 1)..arr.len() {
186                    if arr[i] == arr[j] {
187                        errors.push(
188                            IssueCode::Custom {
189                                code: "not_unique".to_string(),
190                            },
191                            "Array items must be unique",
192                        );
193                        break;
194                    }
195                }
196            }
197        }
198
199        if let Some(ref contains) = self.contains {
200            let count = arr.iter().filter(|v| *v == contains).count();
201            if count == 0 {
202                errors.push(
203                    IssueCode::Custom {
204                        code: "missing_contains".to_string(),
205                    },
206                    "Array must contain required value",
207                );
208            }
209            if let Some(min_contains) = self.min_contains {
210                if count < min_contains {
211                    errors.push(
212                        IssueCode::TooSmall {
213                            minimum: min_contains as f64,
214                            inclusive: true,
215                        },
216                        format!(
217                            "Array must contain required value at least {} time(s)",
218                            min_contains
219                        ),
220                    );
221                }
222            }
223            if let Some(max_contains) = self.max_contains {
224                if count > max_contains {
225                    errors.push(
226                        IssueCode::TooBig {
227                            maximum: max_contains as f64,
228                            inclusive: true,
229                        },
230                        format!(
231                            "Array must contain required value at most {} time(s)",
232                            max_contains
233                        ),
234                    );
235                }
236            }
237        }
238
239        // Validate each element
240        let mut results = Vec::with_capacity(arr.len());
241
242        for (i, item) in arr.iter().enumerate() {
243            match self.element.parse_value(item) {
244                Ok(v) => results.push(v),
245                Err(e) => {
246                    errors = errors.merge(e.with_prefix(PathSegment::Index(i)));
247                }
248            }
249        }
250
251        if errors.is_empty() {
252            Ok(results)
253        } else {
254            Err(errors)
255        }
256    }
257}