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}