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}