Skip to main content

qubit_argument/argument/
string_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 string arguments.
9
10#[cfg(feature = "regex")]
11use regex::Regex;
12
13use crate::argument::ArgumentError;
14use crate::argument::ArgumentErrorKind;
15use crate::argument::ArgumentResult;
16use crate::argument::LengthConstraint;
17use crate::argument::LengthMetric;
18#[cfg(feature = "regex")]
19use crate::argument::PatternExpectation;
20use crate::argument::internal::Sealed;
21
22/// Validates string arguments while preserving ownership or borrowing.
23///
24/// Byte-length methods count UTF-8 bytes, while character-count methods count
25/// Unicode scalar values. Every successful method returns the original value
26/// without cloning it. String contents are inspected but never captured in an
27/// error. Length failures use [`LengthMetric::Bytes`] for byte methods and
28/// [`LengthMetric::UnicodeScalars`] for character-count methods.
29///
30/// The trait is sealed and implemented only for `String` and `&str`.
31///
32/// # Examples
33///
34/// ```
35/// use qubit_argument::StringArgument;
36///
37/// let value = "hello".require_non_blank("name").unwrap();
38/// assert_eq!(value, "hello");
39/// ```
40pub trait StringArgument: Sealed + Sized {
41    /// Requires this string to contain at least one non-whitespace character.
42    ///
43    /// Success returns the original value without cloning. Empty strings and
44    /// strings whose Unicode scalar values are all whitespace return
45    /// [`ArgumentErrorKind::Blank`] at `path`.
46    fn require_non_blank(self, path: &str) -> ArgumentResult<Self>;
47
48    /// Requires this string to contain exactly `expected` UTF-8 bytes.
49    ///
50    /// Success returns the original value without cloning. A different byte
51    /// length returns [`ArgumentErrorKind::Length`] at `path`.
52    fn require_byte_len(self, path: &str, expected: usize) -> ArgumentResult<Self>;
53
54    /// Requires this string to contain at least `min` UTF-8 bytes.
55    ///
56    /// Success returns the original value without cloning. A smaller byte
57    /// length returns [`ArgumentErrorKind::Length`] at `path`.
58    fn require_byte_len_at_least(self, path: &str, min: usize) -> ArgumentResult<Self>;
59
60    /// Requires this string to contain at most `max` UTF-8 bytes.
61    ///
62    /// Success returns the original value without cloning. A larger byte
63    /// length returns [`ArgumentErrorKind::Length`] at `path`.
64    fn require_byte_len_at_most(self, path: &str, max: usize) -> ArgumentResult<Self>;
65
66    /// Requires this string's UTF-8 byte length to lie in `min..=max`.
67    ///
68    /// The range is validated before the string length. If `min > max`, this
69    /// returns [`ArgumentErrorKind::InvalidLengthConstraint`] at `path`;
70    /// otherwise, an out-of-range length returns
71    /// [`ArgumentErrorKind::Length`]. Success returns the original value
72    /// without cloning.
73    fn require_byte_len_in(self, path: &str, min: usize, max: usize) -> ArgumentResult<Self>;
74
75    /// Requires this string to contain exactly `expected` Unicode scalar
76    /// values.
77    ///
78    /// Success returns the original value without cloning. A different scalar
79    /// count returns [`ArgumentErrorKind::Length`] at `path`.
80    fn require_char_count(self, path: &str, expected: usize) -> ArgumentResult<Self>;
81
82    /// Requires this string to contain at least `min` Unicode scalar values.
83    ///
84    /// Success returns the original value without cloning. A smaller scalar
85    /// count returns [`ArgumentErrorKind::Length`] at `path`.
86    fn require_char_count_at_least(self, path: &str, min: usize) -> ArgumentResult<Self>;
87
88    /// Requires this string to contain at most `max` Unicode scalar values.
89    ///
90    /// Success returns the original value without cloning. A larger scalar
91    /// count returns [`ArgumentErrorKind::Length`] at `path`.
92    fn require_char_count_at_most(self, path: &str, max: usize) -> ArgumentResult<Self>;
93
94    /// Requires this string's Unicode scalar count to lie in `min..=max`.
95    ///
96    /// The range is validated before the character count. If `min > max`, this
97    /// returns [`ArgumentErrorKind::InvalidLengthConstraint`] at `path`;
98    /// otherwise, an out-of-range count returns [`ArgumentErrorKind::Length`].
99    /// Success returns the original value without cloning.
100    fn require_char_count_in(self, path: &str, min: usize, max: usize) -> ArgumentResult<Self>;
101
102    /// Requires this string to match `pattern`.
103    ///
104    /// Matching uses [`Regex::is_match`] without implicit anchoring. Success
105    /// returns the original value without cloning; failure returns
106    /// [`ArgumentErrorKind::Pattern`] at `path` without capturing the input.
107    #[cfg(feature = "regex")]
108    fn require_match(self, path: &str, pattern: &Regex) -> ArgumentResult<Self>;
109
110    /// Requires this string not to match `pattern`.
111    ///
112    /// Matching uses [`Regex::is_match`] without implicit anchoring. Success
113    /// returns the original value without cloning; failure returns
114    /// [`ArgumentErrorKind::Pattern`] at `path` without capturing the input.
115    #[cfg(feature = "regex")]
116    fn require_not_match(self, path: &str, pattern: &Regex) -> ArgumentResult<Self>;
117}
118
119impl StringArgument for &str {
120    /// Validates Unicode blankness and returns the original borrow.
121    ///
122    /// `path` identifies a [`ArgumentErrorKind::Blank`] failure.
123    fn require_non_blank(self, path: &str) -> ArgumentResult<Self> {
124        validate_non_blank(self, path)?;
125        Ok(self)
126    }
127
128    /// Validates the exact UTF-8 byte length and returns the original borrow.
129    ///
130    /// A mismatch returns [`ArgumentErrorKind::Length`] at `path`.
131    #[inline]
132    fn require_byte_len(self, path: &str, expected: usize) -> ArgumentResult<Self> {
133        validate_length(path, self.len(), LengthConstraint::Exact(expected), LengthMetric::Bytes)?;
134        Ok(self)
135    }
136
137    /// Validates the minimum UTF-8 byte length and returns the original borrow.
138    ///
139    /// A value below `min` returns [`ArgumentErrorKind::Length`] at `path`.
140    #[inline]
141    fn require_byte_len_at_least(self, path: &str, min: usize) -> ArgumentResult<Self> {
142        validate_length(path, self.len(), LengthConstraint::AtLeast(min), LengthMetric::Bytes)?;
143        Ok(self)
144    }
145
146    /// Validates the maximum UTF-8 byte length and returns the original borrow.
147    ///
148    /// A value above `max` returns [`ArgumentErrorKind::Length`] at `path`.
149    #[inline]
150    fn require_byte_len_at_most(self, path: &str, max: usize) -> ArgumentResult<Self> {
151        validate_length(path, self.len(), LengthConstraint::AtMost(max), LengthMetric::Bytes)?;
152        Ok(self)
153    }
154
155    /// Validates an inclusive UTF-8 byte-length range and returns the borrow.
156    ///
157    /// `min > max` returns [`ArgumentErrorKind::InvalidLengthConstraint`] at
158    /// `path`; an out-of-range value returns [`ArgumentErrorKind::Length`].
159    #[inline]
160    fn require_byte_len_in(self, path: &str, min: usize, max: usize) -> ArgumentResult<Self> {
161        validate_length(
162            path,
163            self.len(),
164            LengthConstraint::InRange { min, max },
165            LengthMetric::Bytes,
166        )?;
167        Ok(self)
168    }
169
170    /// Validates the exact Unicode scalar count and returns the original
171    /// borrow.
172    ///
173    /// A mismatch returns [`ArgumentErrorKind::Length`] at `path`.
174    fn require_char_count(self, path: &str, expected: usize) -> ArgumentResult<Self> {
175        validate_length(
176            path,
177            self.chars().count(),
178            LengthConstraint::Exact(expected),
179            LengthMetric::UnicodeScalars,
180        )?;
181        Ok(self)
182    }
183
184    /// Validates the minimum Unicode scalar count and returns the original
185    /// borrow.
186    ///
187    /// A count below `min` returns [`ArgumentErrorKind::Length`] at `path`.
188    fn require_char_count_at_least(self, path: &str, min: usize) -> ArgumentResult<Self> {
189        validate_length(
190            path,
191            self.chars().count(),
192            LengthConstraint::AtLeast(min),
193            LengthMetric::UnicodeScalars,
194        )?;
195        Ok(self)
196    }
197
198    /// Validates the maximum Unicode scalar count and returns the original
199    /// borrow.
200    ///
201    /// A count above `max` returns [`ArgumentErrorKind::Length`] at `path`.
202    fn require_char_count_at_most(self, path: &str, max: usize) -> ArgumentResult<Self> {
203        validate_length(
204            path,
205            self.chars().count(),
206            LengthConstraint::AtMost(max),
207            LengthMetric::UnicodeScalars,
208        )?;
209        Ok(self)
210    }
211
212    /// Validates an inclusive Unicode scalar-count range and returns the
213    /// borrow.
214    ///
215    /// `min > max` returns [`ArgumentErrorKind::InvalidLengthConstraint`] at
216    /// `path`; an out-of-range count returns [`ArgumentErrorKind::Length`].
217    fn require_char_count_in(self, path: &str, min: usize, max: usize) -> ArgumentResult<Self> {
218        validate_length(
219            path,
220            self.chars().count(),
221            LengthConstraint::InRange { min, max },
222            LengthMetric::UnicodeScalars,
223        )?;
224        Ok(self)
225    }
226
227    /// Validates a required regex match and returns the original borrow.
228    ///
229    /// A non-match returns [`ArgumentErrorKind::Pattern`] at `path` without
230    /// capturing the input string.
231    #[cfg(feature = "regex")]
232    fn require_match(self, path: &str, pattern: &Regex) -> ArgumentResult<Self> {
233        validate_pattern(self, path, pattern, PatternExpectation::Match)?;
234        Ok(self)
235    }
236
237    /// Validates a required regex non-match and returns the original borrow.
238    ///
239    /// A match returns [`ArgumentErrorKind::Pattern`] at `path` without
240    /// capturing the input string.
241    #[cfg(feature = "regex")]
242    fn require_not_match(self, path: &str, pattern: &Regex) -> ArgumentResult<Self> {
243        validate_pattern(self, path, pattern, PatternExpectation::NoMatch)?;
244        Ok(self)
245    }
246}
247
248impl StringArgument for String {
249    /// Validates Unicode blankness and returns the original owned string.
250    ///
251    /// `path` identifies a [`ArgumentErrorKind::Blank`] failure.
252    fn require_non_blank(self, path: &str) -> ArgumentResult<Self> {
253        validate_non_blank(self.as_str(), path)?;
254        Ok(self)
255    }
256
257    /// Validates the exact UTF-8 byte length and returns the owned string.
258    ///
259    /// A mismatch returns [`ArgumentErrorKind::Length`] at `path`.
260    #[inline]
261    fn require_byte_len(self, path: &str, expected: usize) -> ArgumentResult<Self> {
262        validate_length(path, self.len(), LengthConstraint::Exact(expected), LengthMetric::Bytes)?;
263        Ok(self)
264    }
265
266    /// Validates the minimum UTF-8 byte length and returns the owned string.
267    ///
268    /// A value below `min` returns [`ArgumentErrorKind::Length`] at `path`.
269    #[inline]
270    fn require_byte_len_at_least(self, path: &str, min: usize) -> ArgumentResult<Self> {
271        validate_length(path, self.len(), LengthConstraint::AtLeast(min), LengthMetric::Bytes)?;
272        Ok(self)
273    }
274
275    /// Validates the maximum UTF-8 byte length and returns the owned string.
276    ///
277    /// A value above `max` returns [`ArgumentErrorKind::Length`] at `path`.
278    #[inline]
279    fn require_byte_len_at_most(self, path: &str, max: usize) -> ArgumentResult<Self> {
280        validate_length(path, self.len(), LengthConstraint::AtMost(max), LengthMetric::Bytes)?;
281        Ok(self)
282    }
283
284    /// Validates an inclusive UTF-8 byte-length range and returns the string.
285    ///
286    /// `min > max` returns [`ArgumentErrorKind::InvalidLengthConstraint`] at
287    /// `path`; an out-of-range value returns [`ArgumentErrorKind::Length`].
288    #[inline]
289    fn require_byte_len_in(self, path: &str, min: usize, max: usize) -> ArgumentResult<Self> {
290        validate_length(
291            path,
292            self.len(),
293            LengthConstraint::InRange { min, max },
294            LengthMetric::Bytes,
295        )?;
296        Ok(self)
297    }
298
299    /// Validates the exact Unicode scalar count and returns the owned string.
300    ///
301    /// A mismatch returns [`ArgumentErrorKind::Length`] at `path`.
302    fn require_char_count(self, path: &str, expected: usize) -> ArgumentResult<Self> {
303        validate_length(
304            path,
305            self.chars().count(),
306            LengthConstraint::Exact(expected),
307            LengthMetric::UnicodeScalars,
308        )?;
309        Ok(self)
310    }
311
312    /// Validates the minimum Unicode scalar count and returns the owned string.
313    ///
314    /// A count below `min` returns [`ArgumentErrorKind::Length`] at `path`.
315    fn require_char_count_at_least(self, path: &str, min: usize) -> ArgumentResult<Self> {
316        validate_length(
317            path,
318            self.chars().count(),
319            LengthConstraint::AtLeast(min),
320            LengthMetric::UnicodeScalars,
321        )?;
322        Ok(self)
323    }
324
325    /// Validates the maximum Unicode scalar count and returns the owned string.
326    ///
327    /// A count above `max` returns [`ArgumentErrorKind::Length`] at `path`.
328    fn require_char_count_at_most(self, path: &str, max: usize) -> ArgumentResult<Self> {
329        validate_length(
330            path,
331            self.chars().count(),
332            LengthConstraint::AtMost(max),
333            LengthMetric::UnicodeScalars,
334        )?;
335        Ok(self)
336    }
337
338    /// Validates an inclusive Unicode scalar-count range and returns the
339    /// string.
340    ///
341    /// `min > max` returns [`ArgumentErrorKind::InvalidLengthConstraint`] at
342    /// `path`; an out-of-range count returns [`ArgumentErrorKind::Length`].
343    fn require_char_count_in(self, path: &str, min: usize, max: usize) -> ArgumentResult<Self> {
344        validate_length(
345            path,
346            self.chars().count(),
347            LengthConstraint::InRange { min, max },
348            LengthMetric::UnicodeScalars,
349        )?;
350        Ok(self)
351    }
352
353    /// Validates a required regex match and returns the owned string.
354    ///
355    /// A non-match returns [`ArgumentErrorKind::Pattern`] at `path` without
356    /// capturing the input string.
357    #[cfg(feature = "regex")]
358    fn require_match(self, path: &str, pattern: &Regex) -> ArgumentResult<Self> {
359        validate_pattern(self.as_str(), path, pattern, PatternExpectation::Match)?;
360        Ok(self)
361    }
362
363    /// Validates a required regex non-match and returns the owned string.
364    ///
365    /// A match returns [`ArgumentErrorKind::Pattern`] at `path` without
366    /// capturing the input string.
367    #[cfg(feature = "regex")]
368    fn require_not_match(self, path: &str, pattern: &Regex) -> ArgumentResult<Self> {
369        validate_pattern(self.as_str(), path, pattern, PatternExpectation::NoMatch)?;
370        Ok(self)
371    }
372}
373
374/// Validates that `value` contains a non-whitespace Unicode scalar value.
375///
376/// `value` is inspected without allocation. The function returns `Ok(())`
377/// when at least one scalar value is not whitespace; otherwise, it returns
378/// [`ArgumentErrorKind::Blank`] at `path` without storing `value`.
379fn validate_non_blank(value: &str, path: &str) -> ArgumentResult<()> {
380    if value.chars().all(char::is_whitespace) {
381        Err(ArgumentError::new(path, ArgumentErrorKind::Blank))
382    } else {
383        Ok(())
384    }
385}
386
387/// Validates an observed length against a structured length constraint.
388///
389/// `actual` is compared with `constraint`, `metric` records how it was
390/// measured, and `path` identifies any failure. A reversed inclusive range
391/// returns
392/// [`ArgumentErrorKind::InvalidLengthConstraint`] before `actual` is checked.
393/// Any other unsatisfied constraint returns [`ArgumentErrorKind::Length`]; a
394/// satisfied constraint returns `Ok(())`.
395fn validate_length(
396    path: &str,
397    actual: usize,
398    constraint: LengthConstraint,
399    metric: LengthMetric,
400) -> ArgumentResult<()> {
401    if let LengthConstraint::InRange { min, max } = &constraint
402        && min > max
403    {
404        return Err(ArgumentError::new(
405            path,
406            ArgumentErrorKind::InvalidLengthConstraint { constraint, metric },
407        ));
408    }
409
410    let is_valid = match &constraint {
411        LengthConstraint::Exact(expected) => actual == *expected,
412        LengthConstraint::AtLeast(min) => actual >= *min,
413        LengthConstraint::AtMost(max) => actual <= *max,
414        LengthConstraint::InRange { min, max } => actual >= *min && actual <= *max,
415    };
416    if is_valid {
417        Ok(())
418    } else {
419        Err(ArgumentError::new(
420            path,
421            ArgumentErrorKind::Length {
422                actual,
423                constraint,
424                metric,
425            },
426        ))
427    }
428}
429
430/// Validates one regex expectation without retaining the inspected string.
431///
432/// `value` is tested with `pattern` according to `expectation`, and `path`
433/// identifies any failure. Success returns `Ok(())`; failure returns
434/// [`ArgumentErrorKind::Pattern`] containing only the pattern text and
435/// expectation, never `value`.
436#[cfg(feature = "regex")]
437fn validate_pattern(value: &str, path: &str, pattern: &Regex, expectation: PatternExpectation) -> ArgumentResult<()> {
438    let matches = pattern.is_match(value);
439    let is_valid = match expectation {
440        PatternExpectation::Match => matches,
441        PatternExpectation::NoMatch => !matches,
442    };
443    if is_valid {
444        Ok(())
445    } else {
446        Err(ArgumentError::new(
447            path,
448            ArgumentErrorKind::Pattern {
449                pattern: String::from(pattern.as_str()),
450                expectation,
451            },
452        ))
453    }
454}