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
10use crate::argument::{
11    ArgumentError,
12    ArgumentErrorKind,
13    ArgumentResult,
14    LengthConstraint,
15    LengthMetric,
16    sealed::Sealed,
17};
18
19#[cfg(feature = "regex")]
20use crate::argument::PatternExpectation;
21#[cfg(feature = "regex")]
22use regex::Regex;
23
24/// Validates string arguments while preserving ownership or borrowing.
25///
26/// Byte-length methods count UTF-8 bytes, while character-count methods count
27/// Unicode scalar values. Every successful method returns the original value
28/// without cloning it. String contents are inspected but never captured in an
29/// error. Length failures use [`LengthMetric::Bytes`] for byte methods and
30/// [`LengthMetric::UnicodeScalars`] for character-count methods.
31///
32/// The trait is sealed and implemented only for `String` and `&str`.
33pub trait StringArgument: Sealed + Sized {
34    /// Requires this string to contain at least one non-whitespace character.
35    ///
36    /// Success returns the original value without cloning. Empty strings and
37    /// strings whose Unicode scalar values are all whitespace return
38    /// [`ArgumentErrorKind::Blank`] at `path`.
39    fn require_non_blank(self, path: &str) -> ArgumentResult<Self>;
40
41    /// Requires this string to contain exactly `expected` UTF-8 bytes.
42    ///
43    /// Success returns the original value without cloning. A different byte
44    /// length returns [`ArgumentErrorKind::Length`] at `path`.
45    fn require_byte_len(
46        self,
47        path: &str,
48        expected: usize,
49    ) -> ArgumentResult<Self>;
50
51    /// Requires this string to contain at least `min` UTF-8 bytes.
52    ///
53    /// Success returns the original value without cloning. A smaller byte
54    /// length returns [`ArgumentErrorKind::Length`] at `path`.
55    fn require_byte_len_at_least(
56        self,
57        path: &str,
58        min: usize,
59    ) -> ArgumentResult<Self>;
60
61    /// Requires this string to contain at most `max` UTF-8 bytes.
62    ///
63    /// Success returns the original value without cloning. A larger byte
64    /// length returns [`ArgumentErrorKind::Length`] at `path`.
65    fn require_byte_len_at_most(
66        self,
67        path: &str,
68        max: usize,
69    ) -> ArgumentResult<Self>;
70
71    /// Requires this string's UTF-8 byte length to lie in `min..=max`.
72    ///
73    /// The range is validated before the string length. If `min > max`, this
74    /// returns [`ArgumentErrorKind::InvalidLengthConstraint`] at `path`;
75    /// otherwise, an out-of-range length returns
76    /// [`ArgumentErrorKind::Length`]. Success returns the original value
77    /// without cloning.
78    fn require_byte_len_in(
79        self,
80        path: &str,
81        min: usize,
82        max: usize,
83    ) -> ArgumentResult<Self>;
84
85    /// Requires this string to contain exactly `expected` Unicode scalar
86    /// values.
87    ///
88    /// Success returns the original value without cloning. A different scalar
89    /// count returns [`ArgumentErrorKind::Length`] at `path`.
90    fn require_char_count(
91        self,
92        path: &str,
93        expected: usize,
94    ) -> ArgumentResult<Self>;
95
96    /// Requires this string to contain at least `min` Unicode scalar values.
97    ///
98    /// Success returns the original value without cloning. A smaller scalar
99    /// count returns [`ArgumentErrorKind::Length`] at `path`.
100    fn require_char_count_at_least(
101        self,
102        path: &str,
103        min: usize,
104    ) -> ArgumentResult<Self>;
105
106    /// Requires this string to contain at most `max` Unicode scalar values.
107    ///
108    /// Success returns the original value without cloning. A larger scalar
109    /// count returns [`ArgumentErrorKind::Length`] at `path`.
110    fn require_char_count_at_most(
111        self,
112        path: &str,
113        max: usize,
114    ) -> ArgumentResult<Self>;
115
116    /// Requires this string's Unicode scalar count to lie in `min..=max`.
117    ///
118    /// The range is validated before the character count. If `min > max`, this
119    /// returns [`ArgumentErrorKind::InvalidLengthConstraint`] at `path`;
120    /// otherwise, an out-of-range count returns [`ArgumentErrorKind::Length`].
121    /// Success returns the original value without cloning.
122    fn require_char_count_in(
123        self,
124        path: &str,
125        min: usize,
126        max: usize,
127    ) -> ArgumentResult<Self>;
128
129    /// Requires this string to match `pattern`.
130    ///
131    /// Matching uses [`Regex::is_match`] without implicit anchoring. Success
132    /// returns the original value without cloning; failure returns
133    /// [`ArgumentErrorKind::Pattern`] at `path` without capturing the input.
134    #[cfg(feature = "regex")]
135    fn require_match(self, path: &str, pattern: &Regex)
136    -> ArgumentResult<Self>;
137
138    /// Requires this string not to match `pattern`.
139    ///
140    /// Matching uses [`Regex::is_match`] without implicit anchoring. Success
141    /// returns the original value without cloning; failure returns
142    /// [`ArgumentErrorKind::Pattern`] at `path` without capturing the input.
143    #[cfg(feature = "regex")]
144    fn require_not_match(
145        self,
146        path: &str,
147        pattern: &Regex,
148    ) -> ArgumentResult<Self>;
149}
150
151impl StringArgument for &str {
152    /// Validates Unicode blankness and returns the original borrow.
153    ///
154    /// `path` identifies a [`ArgumentErrorKind::Blank`] failure.
155    #[inline]
156    fn require_non_blank(self, path: &str) -> ArgumentResult<Self> {
157        validate_non_blank(self, path)?;
158        Ok(self)
159    }
160
161    /// Validates the exact UTF-8 byte length and returns the original borrow.
162    ///
163    /// A mismatch returns [`ArgumentErrorKind::Length`] at `path`.
164    #[inline]
165    fn require_byte_len(
166        self,
167        path: &str,
168        expected: usize,
169    ) -> ArgumentResult<Self> {
170        validate_length(
171            path,
172            self.len(),
173            LengthConstraint::Exact(expected),
174            LengthMetric::Bytes,
175        )?;
176        Ok(self)
177    }
178
179    /// Validates the minimum UTF-8 byte length and returns the original borrow.
180    ///
181    /// A value below `min` returns [`ArgumentErrorKind::Length`] at `path`.
182    #[inline]
183    fn require_byte_len_at_least(
184        self,
185        path: &str,
186        min: usize,
187    ) -> ArgumentResult<Self> {
188        validate_length(
189            path,
190            self.len(),
191            LengthConstraint::AtLeast(min),
192            LengthMetric::Bytes,
193        )?;
194        Ok(self)
195    }
196
197    /// Validates the maximum UTF-8 byte length and returns the original borrow.
198    ///
199    /// A value above `max` returns [`ArgumentErrorKind::Length`] at `path`.
200    #[inline]
201    fn require_byte_len_at_most(
202        self,
203        path: &str,
204        max: usize,
205    ) -> ArgumentResult<Self> {
206        validate_length(
207            path,
208            self.len(),
209            LengthConstraint::AtMost(max),
210            LengthMetric::Bytes,
211        )?;
212        Ok(self)
213    }
214
215    /// Validates an inclusive UTF-8 byte-length range and returns the borrow.
216    ///
217    /// `min > max` returns [`ArgumentErrorKind::InvalidLengthConstraint`] at
218    /// `path`; an out-of-range value returns [`ArgumentErrorKind::Length`].
219    #[inline]
220    fn require_byte_len_in(
221        self,
222        path: &str,
223        min: usize,
224        max: usize,
225    ) -> ArgumentResult<Self> {
226        validate_length(
227            path,
228            self.len(),
229            LengthConstraint::InRange { min, max },
230            LengthMetric::Bytes,
231        )?;
232        Ok(self)
233    }
234
235    /// Validates the exact Unicode scalar count and returns the original
236    /// borrow.
237    ///
238    /// A mismatch returns [`ArgumentErrorKind::Length`] at `path`.
239    #[inline]
240    fn require_char_count(
241        self,
242        path: &str,
243        expected: usize,
244    ) -> ArgumentResult<Self> {
245        validate_length(
246            path,
247            self.chars().count(),
248            LengthConstraint::Exact(expected),
249            LengthMetric::UnicodeScalars,
250        )?;
251        Ok(self)
252    }
253
254    /// Validates the minimum Unicode scalar count and returns the original
255    /// borrow.
256    ///
257    /// A count below `min` returns [`ArgumentErrorKind::Length`] at `path`.
258    #[inline]
259    fn require_char_count_at_least(
260        self,
261        path: &str,
262        min: usize,
263    ) -> ArgumentResult<Self> {
264        validate_length(
265            path,
266            self.chars().count(),
267            LengthConstraint::AtLeast(min),
268            LengthMetric::UnicodeScalars,
269        )?;
270        Ok(self)
271    }
272
273    /// Validates the maximum Unicode scalar count and returns the original
274    /// borrow.
275    ///
276    /// A count above `max` returns [`ArgumentErrorKind::Length`] at `path`.
277    #[inline]
278    fn require_char_count_at_most(
279        self,
280        path: &str,
281        max: usize,
282    ) -> ArgumentResult<Self> {
283        validate_length(
284            path,
285            self.chars().count(),
286            LengthConstraint::AtMost(max),
287            LengthMetric::UnicodeScalars,
288        )?;
289        Ok(self)
290    }
291
292    /// Validates an inclusive Unicode scalar-count range and returns the
293    /// borrow.
294    ///
295    /// `min > max` returns [`ArgumentErrorKind::InvalidLengthConstraint`] at
296    /// `path`; an out-of-range count returns [`ArgumentErrorKind::Length`].
297    #[inline]
298    fn require_char_count_in(
299        self,
300        path: &str,
301        min: usize,
302        max: usize,
303    ) -> ArgumentResult<Self> {
304        validate_length(
305            path,
306            self.chars().count(),
307            LengthConstraint::InRange { min, max },
308            LengthMetric::UnicodeScalars,
309        )?;
310        Ok(self)
311    }
312
313    /// Validates a required regex match and returns the original borrow.
314    ///
315    /// A non-match returns [`ArgumentErrorKind::Pattern`] at `path` without
316    /// capturing the input string.
317    #[cfg(feature = "regex")]
318    #[inline]
319    fn require_match(
320        self,
321        path: &str,
322        pattern: &Regex,
323    ) -> ArgumentResult<Self> {
324        validate_pattern(self, path, pattern, PatternExpectation::Match)?;
325        Ok(self)
326    }
327
328    /// Validates a required regex non-match and returns the original borrow.
329    ///
330    /// A match returns [`ArgumentErrorKind::Pattern`] at `path` without
331    /// capturing the input string.
332    #[cfg(feature = "regex")]
333    #[inline]
334    fn require_not_match(
335        self,
336        path: &str,
337        pattern: &Regex,
338    ) -> ArgumentResult<Self> {
339        validate_pattern(self, path, pattern, PatternExpectation::NoMatch)?;
340        Ok(self)
341    }
342}
343
344impl StringArgument for String {
345    /// Validates Unicode blankness and returns the original owned string.
346    ///
347    /// `path` identifies a [`ArgumentErrorKind::Blank`] failure.
348    #[inline]
349    fn require_non_blank(self, path: &str) -> ArgumentResult<Self> {
350        validate_non_blank(self.as_str(), path)?;
351        Ok(self)
352    }
353
354    /// Validates the exact UTF-8 byte length and returns the owned string.
355    ///
356    /// A mismatch returns [`ArgumentErrorKind::Length`] at `path`.
357    #[inline]
358    fn require_byte_len(
359        self,
360        path: &str,
361        expected: usize,
362    ) -> ArgumentResult<Self> {
363        validate_length(
364            path,
365            self.len(),
366            LengthConstraint::Exact(expected),
367            LengthMetric::Bytes,
368        )?;
369        Ok(self)
370    }
371
372    /// Validates the minimum UTF-8 byte length and returns the owned string.
373    ///
374    /// A value below `min` returns [`ArgumentErrorKind::Length`] at `path`.
375    #[inline]
376    fn require_byte_len_at_least(
377        self,
378        path: &str,
379        min: usize,
380    ) -> ArgumentResult<Self> {
381        validate_length(
382            path,
383            self.len(),
384            LengthConstraint::AtLeast(min),
385            LengthMetric::Bytes,
386        )?;
387        Ok(self)
388    }
389
390    /// Validates the maximum UTF-8 byte length and returns the owned string.
391    ///
392    /// A value above `max` returns [`ArgumentErrorKind::Length`] at `path`.
393    #[inline]
394    fn require_byte_len_at_most(
395        self,
396        path: &str,
397        max: usize,
398    ) -> ArgumentResult<Self> {
399        validate_length(
400            path,
401            self.len(),
402            LengthConstraint::AtMost(max),
403            LengthMetric::Bytes,
404        )?;
405        Ok(self)
406    }
407
408    /// Validates an inclusive UTF-8 byte-length range and returns the string.
409    ///
410    /// `min > max` returns [`ArgumentErrorKind::InvalidLengthConstraint`] at
411    /// `path`; an out-of-range value returns [`ArgumentErrorKind::Length`].
412    #[inline]
413    fn require_byte_len_in(
414        self,
415        path: &str,
416        min: usize,
417        max: usize,
418    ) -> ArgumentResult<Self> {
419        validate_length(
420            path,
421            self.len(),
422            LengthConstraint::InRange { min, max },
423            LengthMetric::Bytes,
424        )?;
425        Ok(self)
426    }
427
428    /// Validates the exact Unicode scalar count and returns the owned string.
429    ///
430    /// A mismatch returns [`ArgumentErrorKind::Length`] at `path`.
431    #[inline]
432    fn require_char_count(
433        self,
434        path: &str,
435        expected: usize,
436    ) -> ArgumentResult<Self> {
437        validate_length(
438            path,
439            self.chars().count(),
440            LengthConstraint::Exact(expected),
441            LengthMetric::UnicodeScalars,
442        )?;
443        Ok(self)
444    }
445
446    /// Validates the minimum Unicode scalar count and returns the owned string.
447    ///
448    /// A count below `min` returns [`ArgumentErrorKind::Length`] at `path`.
449    #[inline]
450    fn require_char_count_at_least(
451        self,
452        path: &str,
453        min: usize,
454    ) -> ArgumentResult<Self> {
455        validate_length(
456            path,
457            self.chars().count(),
458            LengthConstraint::AtLeast(min),
459            LengthMetric::UnicodeScalars,
460        )?;
461        Ok(self)
462    }
463
464    /// Validates the maximum Unicode scalar count and returns the owned string.
465    ///
466    /// A count above `max` returns [`ArgumentErrorKind::Length`] at `path`.
467    #[inline]
468    fn require_char_count_at_most(
469        self,
470        path: &str,
471        max: usize,
472    ) -> ArgumentResult<Self> {
473        validate_length(
474            path,
475            self.chars().count(),
476            LengthConstraint::AtMost(max),
477            LengthMetric::UnicodeScalars,
478        )?;
479        Ok(self)
480    }
481
482    /// Validates an inclusive Unicode scalar-count range and returns the
483    /// string.
484    ///
485    /// `min > max` returns [`ArgumentErrorKind::InvalidLengthConstraint`] at
486    /// `path`; an out-of-range count returns [`ArgumentErrorKind::Length`].
487    #[inline]
488    fn require_char_count_in(
489        self,
490        path: &str,
491        min: usize,
492        max: usize,
493    ) -> ArgumentResult<Self> {
494        validate_length(
495            path,
496            self.chars().count(),
497            LengthConstraint::InRange { min, max },
498            LengthMetric::UnicodeScalars,
499        )?;
500        Ok(self)
501    }
502
503    /// Validates a required regex match and returns the owned string.
504    ///
505    /// A non-match returns [`ArgumentErrorKind::Pattern`] at `path` without
506    /// capturing the input string.
507    #[cfg(feature = "regex")]
508    #[inline]
509    fn require_match(
510        self,
511        path: &str,
512        pattern: &Regex,
513    ) -> ArgumentResult<Self> {
514        validate_pattern(
515            self.as_str(),
516            path,
517            pattern,
518            PatternExpectation::Match,
519        )?;
520        Ok(self)
521    }
522
523    /// Validates a required regex non-match and returns the owned string.
524    ///
525    /// A match returns [`ArgumentErrorKind::Pattern`] at `path` without
526    /// capturing the input string.
527    #[cfg(feature = "regex")]
528    #[inline]
529    fn require_not_match(
530        self,
531        path: &str,
532        pattern: &Regex,
533    ) -> ArgumentResult<Self> {
534        validate_pattern(
535            self.as_str(),
536            path,
537            pattern,
538            PatternExpectation::NoMatch,
539        )?;
540        Ok(self)
541    }
542}
543
544/// Validates that `value` contains a non-whitespace Unicode scalar value.
545///
546/// `value` is inspected without allocation. The function returns `Ok(())`
547/// when at least one scalar value is not whitespace; otherwise, it returns
548/// [`ArgumentErrorKind::Blank`] at `path` without storing `value`.
549fn validate_non_blank(value: &str, path: &str) -> ArgumentResult<()> {
550    if value.chars().all(char::is_whitespace) {
551        Err(ArgumentError::new(path, ArgumentErrorKind::Blank))
552    } else {
553        Ok(())
554    }
555}
556
557/// Validates an observed length against a structured length constraint.
558///
559/// `actual` is compared with `constraint`, `metric` records how it was
560/// measured, and `path` identifies any failure. A reversed inclusive range
561/// returns
562/// [`ArgumentErrorKind::InvalidLengthConstraint`] before `actual` is checked.
563/// Any other unsatisfied constraint returns [`ArgumentErrorKind::Length`]; a
564/// satisfied constraint returns `Ok(())`.
565fn validate_length(
566    path: &str,
567    actual: usize,
568    constraint: LengthConstraint,
569    metric: LengthMetric,
570) -> ArgumentResult<()> {
571    if let LengthConstraint::InRange { min, max } = &constraint
572        && min > max
573    {
574        return Err(ArgumentError::new(
575            path,
576            ArgumentErrorKind::InvalidLengthConstraint { constraint, metric },
577        ));
578    }
579
580    let is_valid = match &constraint {
581        LengthConstraint::Exact(expected) => actual == *expected,
582        LengthConstraint::AtLeast(min) => actual >= *min,
583        LengthConstraint::AtMost(max) => actual <= *max,
584        LengthConstraint::InRange { min, max } => {
585            actual >= *min && actual <= *max
586        }
587    };
588    if is_valid {
589        Ok(())
590    } else {
591        Err(ArgumentError::new(
592            path,
593            ArgumentErrorKind::Length {
594                actual,
595                constraint,
596                metric,
597            },
598        ))
599    }
600}
601
602/// Validates one regex expectation without retaining the inspected string.
603///
604/// `value` is tested with `pattern` according to `expectation`, and `path`
605/// identifies any failure. Success returns `Ok(())`; failure returns
606/// [`ArgumentErrorKind::Pattern`] containing only the pattern text and
607/// expectation, never `value`.
608#[cfg(feature = "regex")]
609fn validate_pattern(
610    value: &str,
611    path: &str,
612    pattern: &Regex,
613    expectation: PatternExpectation,
614) -> ArgumentResult<()> {
615    let matches = pattern.is_match(value);
616    let is_valid = match expectation {
617        PatternExpectation::Match => matches,
618        PatternExpectation::NoMatch => !matches,
619    };
620    if is_valid {
621        Ok(())
622    } else {
623        Err(ArgumentError::new(
624            path,
625            ArgumentErrorKind::Pattern {
626                pattern: String::from(pattern.as_str()),
627                expectation,
628            },
629        ))
630    }
631}