qubit_argument/argument/collection_argument.rs
1// =============================================================================
2// Copyright (c) 2025 - 2026 Haixing Hu.
3//
4// SPDX-License-Identifier: Apache-2.0
5//
6// Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Ownership-preserving validation for collection arguments.
9
10use crate::argument::ArgumentError;
11use crate::argument::ArgumentErrorKind;
12use crate::argument::ArgumentResult;
13use crate::argument::LengthConstraint;
14use crate::argument::LengthMetric;
15use crate::argument::internal::Sealed;
16
17/// Validates collection lengths while preserving the original collection.
18///
19/// Every successful method consumes and returns the same collection value
20/// without cloning its elements. Implementations are provided for owned
21/// vectors, borrowed slices, and arrays. Length failures carry
22/// [`LengthMetric::Elements`].
23///
24/// The trait is sealed to those library-supported collection forms.
25///
26/// # Examples
27///
28/// ```
29/// use qubit_argument::CollectionArgument;
30///
31/// let values = vec![1, 2, 3].require_len("items", 3).unwrap();
32/// assert_eq!(values, [1, 2, 3]);
33/// ```
34pub trait CollectionArgument: Sealed + Sized {
35 /// Requires this collection to contain at least one element.
36 ///
37 /// # Parameters
38 ///
39 /// - `path`: identifies the collection in a validation error.
40 ///
41 /// # Returns
42 ///
43 /// The original collection on success, without cloning its elements.
44 ///
45 /// # Errors
46 ///
47 /// Returns [`ArgumentErrorKind::Empty`] at `path` when the collection is
48 /// empty.
49 fn require_non_empty(self, path: &str) -> ArgumentResult<Self>;
50
51 /// Requires this collection to contain exactly `expected` elements.
52 ///
53 /// # Parameters
54 ///
55 /// - `path`: identifies the collection in a validation error.
56 /// - `expected`: the required number of elements.
57 ///
58 /// # Returns
59 ///
60 /// The original collection on success, without cloning its elements.
61 ///
62 /// # Errors
63 ///
64 /// Returns [`ArgumentErrorKind::Length`] at `path` when the length differs
65 /// from `expected`.
66 fn require_len(self, path: &str, expected: usize) -> ArgumentResult<Self>;
67
68 /// Requires this collection to contain at least `min` elements.
69 ///
70 /// # Parameters
71 ///
72 /// - `path`: identifies the collection in a validation error.
73 /// - `min`: the minimum number of elements.
74 ///
75 /// # Returns
76 ///
77 /// The original collection on success, without cloning its elements.
78 ///
79 /// # Errors
80 ///
81 /// Returns [`ArgumentErrorKind::Length`] at `path` when the length is
82 /// below `min`.
83 fn require_len_at_least(self, path: &str, min: usize) -> ArgumentResult<Self>;
84
85 /// Requires this collection to contain at most `max` elements.
86 ///
87 /// # Parameters
88 ///
89 /// - `path`: identifies the collection in a validation error.
90 /// - `max`: the maximum number of elements.
91 ///
92 /// # Returns
93 ///
94 /// The original collection on success, without cloning its elements.
95 ///
96 /// # Errors
97 ///
98 /// Returns [`ArgumentErrorKind::Length`] at `path` when the length is
99 /// above `max`.
100 fn require_len_at_most(self, path: &str, max: usize) -> ArgumentResult<Self>;
101
102 /// Requires this collection's length to lie in `min..=max`.
103 ///
104 /// # Parameters
105 ///
106 /// - `path`: identifies the collection in a validation error.
107 /// - `min`: the inclusive lower bound.
108 /// - `max`: the inclusive upper bound.
109 ///
110 /// # Returns
111 ///
112 /// The original collection on success, without cloning its elements.
113 ///
114 /// # Errors
115 ///
116 /// The range is validated before the observed length. If `min > max`,
117 /// returns [`ArgumentErrorKind::InvalidLengthConstraint`] at `path`;
118 /// otherwise, an out-of-range length returns [`ArgumentErrorKind::Length`].
119 fn require_len_in(self, path: &str, min: usize, max: usize) -> ArgumentResult<Self>;
120}
121
122impl<T> CollectionArgument for Vec<T> {
123 /// Validates non-emptiness and returns the original vector.
124 ///
125 /// An empty vector returns [`ArgumentErrorKind::Empty`] at `path`.
126 #[inline]
127 fn require_non_empty(self, path: &str) -> ArgumentResult<Self> {
128 if self.is_empty() {
129 return Err(ArgumentError::new(path, ArgumentErrorKind::Empty));
130 }
131 Ok(self)
132 }
133
134 /// Validates the exact length and returns the original vector.
135 ///
136 /// A mismatch returns [`ArgumentErrorKind::Length`] at `path`.
137 #[inline]
138 fn require_len(self, path: &str, expected: usize) -> ArgumentResult<Self> {
139 validate_length(path, self.len(), LengthConstraint::Exact(expected))?;
140 Ok(self)
141 }
142
143 /// Validates the minimum length and returns the original vector.
144 ///
145 /// A length below `min` returns [`ArgumentErrorKind::Length`] at `path`.
146 #[inline]
147 fn require_len_at_least(self, path: &str, min: usize) -> ArgumentResult<Self> {
148 validate_length(path, self.len(), LengthConstraint::AtLeast(min))?;
149 Ok(self)
150 }
151
152 /// Validates the maximum length and returns the original vector.
153 ///
154 /// A length above `max` returns [`ArgumentErrorKind::Length`] at `path`.
155 #[inline]
156 fn require_len_at_most(self, path: &str, max: usize) -> ArgumentResult<Self> {
157 validate_length(path, self.len(), LengthConstraint::AtMost(max))?;
158 Ok(self)
159 }
160
161 /// Validates an inclusive length range and returns the original vector.
162 ///
163 /// A reversed range returns
164 /// [`ArgumentErrorKind::InvalidLengthConstraint`] at `path`; an
165 /// out-of-range length returns [`ArgumentErrorKind::Length`].
166 #[inline]
167 fn require_len_in(self, path: &str, min: usize, max: usize) -> ArgumentResult<Self> {
168 validate_length(path, self.len(), LengthConstraint::InRange { min, max })?;
169 Ok(self)
170 }
171}
172
173impl<T> CollectionArgument for &[T] {
174 /// Validates non-emptiness and returns the original borrowed slice.
175 ///
176 /// An empty slice returns [`ArgumentErrorKind::Empty`] at `path`.
177 #[inline]
178 fn require_non_empty(self, path: &str) -> ArgumentResult<Self> {
179 if self.is_empty() {
180 return Err(ArgumentError::new(path, ArgumentErrorKind::Empty));
181 }
182 Ok(self)
183 }
184
185 /// Validates the exact length and returns the original borrowed slice.
186 ///
187 /// A mismatch returns [`ArgumentErrorKind::Length`] at `path`.
188 #[inline]
189 fn require_len(self, path: &str, expected: usize) -> ArgumentResult<Self> {
190 validate_length(path, self.len(), LengthConstraint::Exact(expected))?;
191 Ok(self)
192 }
193
194 /// Validates the minimum length and returns the original borrowed slice.
195 ///
196 /// A length below `min` returns [`ArgumentErrorKind::Length`] at `path`.
197 #[inline]
198 fn require_len_at_least(self, path: &str, min: usize) -> ArgumentResult<Self> {
199 validate_length(path, self.len(), LengthConstraint::AtLeast(min))?;
200 Ok(self)
201 }
202
203 /// Validates the maximum length and returns the original borrowed slice.
204 ///
205 /// A length above `max` returns [`ArgumentErrorKind::Length`] at `path`.
206 #[inline]
207 fn require_len_at_most(self, path: &str, max: usize) -> ArgumentResult<Self> {
208 validate_length(path, self.len(), LengthConstraint::AtMost(max))?;
209 Ok(self)
210 }
211
212 /// Validates an inclusive length range and returns the borrowed slice.
213 ///
214 /// A reversed range returns
215 /// [`ArgumentErrorKind::InvalidLengthConstraint`] at `path`; an
216 /// out-of-range length returns [`ArgumentErrorKind::Length`].
217 #[inline]
218 fn require_len_in(self, path: &str, min: usize, max: usize) -> ArgumentResult<Self> {
219 validate_length(path, self.len(), LengthConstraint::InRange { min, max })?;
220 Ok(self)
221 }
222}
223
224impl<T, const N: usize> CollectionArgument for [T; N] {
225 /// Validates non-emptiness and returns the original array.
226 ///
227 /// A zero-length array returns [`ArgumentErrorKind::Empty`] at `path`.
228 #[inline]
229 fn require_non_empty(self, path: &str) -> ArgumentResult<Self> {
230 if N == 0 {
231 return Err(ArgumentError::new(path, ArgumentErrorKind::Empty));
232 }
233 Ok(self)
234 }
235
236 /// Validates the exact length and returns the original array.
237 ///
238 /// A mismatch returns [`ArgumentErrorKind::Length`] at `path`.
239 #[inline]
240 fn require_len(self, path: &str, expected: usize) -> ArgumentResult<Self> {
241 validate_length(path, N, LengthConstraint::Exact(expected))?;
242 Ok(self)
243 }
244
245 /// Validates the minimum length and returns the original array.
246 ///
247 /// A length below `min` returns [`ArgumentErrorKind::Length`] at `path`.
248 #[inline]
249 fn require_len_at_least(self, path: &str, min: usize) -> ArgumentResult<Self> {
250 validate_length(path, N, LengthConstraint::AtLeast(min))?;
251 Ok(self)
252 }
253
254 /// Validates the maximum length and returns the original array.
255 ///
256 /// A length above `max` returns [`ArgumentErrorKind::Length`] at `path`.
257 #[inline]
258 fn require_len_at_most(self, path: &str, max: usize) -> ArgumentResult<Self> {
259 validate_length(path, N, LengthConstraint::AtMost(max))?;
260 Ok(self)
261 }
262
263 /// Validates an inclusive length range and returns the original array.
264 ///
265 /// A reversed range returns
266 /// [`ArgumentErrorKind::InvalidLengthConstraint`] at `path`; an
267 /// out-of-range length returns [`ArgumentErrorKind::Length`].
268 #[inline]
269 fn require_len_in(self, path: &str, min: usize, max: usize) -> ArgumentResult<Self> {
270 validate_length(path, N, LengthConstraint::InRange { min, max })?;
271 Ok(self)
272 }
273}
274
275/// Validates an observed collection length against a structured constraint.
276///
277/// # Parameters
278///
279/// - `path`: identifies the collection in a validation error.
280/// - `actual`: the observed number of elements.
281/// - `constraint`: the required relationship between length and element count.
282///
283/// # Returns
284///
285/// Returns `Ok(())` when the constraint is satisfied.
286///
287/// # Errors
288///
289/// A reversed inclusive range returns
290/// [`ArgumentErrorKind::InvalidLengthConstraint`] before `actual` is checked.
291/// Any other unsatisfied constraint returns [`ArgumentErrorKind::Length`].
292fn validate_length(path: &str, actual: usize, constraint: LengthConstraint) -> ArgumentResult<()> {
293 if let LengthConstraint::InRange { min, max } = &constraint
294 && min > max
295 {
296 return Err(ArgumentError::new(
297 path,
298 ArgumentErrorKind::InvalidLengthConstraint {
299 constraint,
300 metric: LengthMetric::Elements,
301 },
302 ));
303 }
304
305 let is_valid = match &constraint {
306 LengthConstraint::Exact(expected) => actual == *expected,
307 LengthConstraint::AtLeast(min) => actual >= *min,
308 LengthConstraint::AtMost(max) => actual <= *max,
309 LengthConstraint::InRange { min, max } => actual >= *min && actual <= *max,
310 };
311 if is_valid {
312 Ok(())
313 } else {
314 Err(ArgumentError::new(
315 path,
316 ArgumentErrorKind::Length {
317 actual,
318 constraint,
319 metric: LengthMetric::Elements,
320 },
321 ))
322 }
323}