Skip to main content

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}