Skip to main content

qubit_argument/argument/
argument_error.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//! Structured errors produced by argument validation.
9
10use std::fmt;
11use std::fmt::Display;
12use std::fmt::Formatter;
13
14use crate::argument::ArgumentBound;
15use crate::argument::ArgumentErrorKind;
16use crate::argument::ArgumentPath;
17use crate::argument::ComparisonConstraint;
18use crate::argument::IndexRole;
19use crate::argument::LengthConstraint;
20use crate::argument::LengthMetric;
21use crate::argument::PatternExpectation;
22use crate::argument::RangeConstraint;
23
24/// A structured argument validation failure.
25///
26/// The error owns its argument path and failure kind, allowing downstream
27/// error types to inspect or preserve it without parsing display text.
28/// [`Display`] escapes caller-provided fields into a single-line diagnostic;
29/// accessors and [`Debug`] continue to expose the original structured values.
30///
31/// # Examples
32///
33/// ```
34/// use qubit_argument::{ArgumentError, ArgumentErrorKind};
35///
36/// let error = ArgumentError::new("value", ArgumentErrorKind::Missing);
37/// assert_eq!(error.to_string(), "argument 'value' is missing");
38/// ```
39///
40/// ```compile_fail
41/// #![deny(unused_must_use)]
42/// use qubit_argument::{ArgumentError, ArgumentErrorKind};
43///
44/// ArgumentError::new("value", ArgumentErrorKind::Missing);
45/// ```
46#[must_use]
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct ArgumentError {
49    /// Owns the location of the argument that failed validation.
50    path: ArgumentPath,
51    /// Owns the structured reason for the validation failure.
52    kind: Box<ArgumentErrorKind>,
53}
54
55impl ArgumentError {
56    /// Creates an error from an argument path and structured failure kind.
57    ///
58    /// `path` is copied only while constructing the error. The supplied
59    /// `kind` is retained unchanged.
60    ///
61    /// # Parameters
62    ///
63    /// - `path`: The path identifying the argument that failed validation.
64    /// - `kind`: The structured reason for the validation failure.
65    ///
66    /// # Returns
67    ///
68    /// An error that owns the path and failure kind.
69    #[inline]
70    pub fn new(path: &str, kind: ArgumentErrorKind) -> Self {
71        Self {
72            path: ArgumentPath::new(path),
73            kind: Box::new(kind),
74        }
75    }
76
77    /// Returns the path of the argument that failed validation.
78    ///
79    /// # Returns
80    ///
81    /// The path borrowed from this error.
82    #[must_use = "the caller should inspect the argument path"]
83    #[inline]
84    pub fn path(&self) -> &ArgumentPath {
85        &self.path
86    }
87
88    /// Returns the structured validation failure kind.
89    ///
90    /// # Returns
91    ///
92    /// The failure kind borrowed from this error.
93    #[must_use = "the caller should inspect the structured error kind"]
94    #[inline]
95    pub fn kind(&self) -> &ArgumentErrorKind {
96        self.kind.as_ref()
97    }
98
99    /// Prepends a parent path to this validation error.
100    ///
101    /// # Parameters
102    ///
103    /// - `prefix`: The parent path to place before the current error path.
104    ///
105    /// # Returns
106    ///
107    /// This error with its path prefixed and its failure kind unchanged.
108    #[inline]
109    pub fn with_path_prefix(mut self, prefix: &str) -> Self {
110        self.path = self.path.with_prefix(prefix);
111        self
112    }
113
114    /// Consumes the error and returns its owned path and failure kind.
115    ///
116    /// The first tuple element is the argument path and the second is the
117    /// structured failure kind.
118    ///
119    /// # Returns
120    ///
121    /// The owned argument path and structured failure kind, in that order.
122    #[inline]
123    pub fn into_parts(self) -> (ArgumentPath, ArgumentErrorKind) {
124        let Self { path, kind } = self;
125        (path, *kind)
126    }
127}
128
129impl Display for ArgumentError {
130    /// Formats a single-line diagnostic entirely from the structured fields.
131    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
132        let path = escape_for_display(self.path.as_ref(), Some('\''));
133        write!(formatter, "argument '{path}'")?;
134        match self.kind.as_ref() {
135            ArgumentErrorKind::Missing => formatter.write_str(" is missing"),
136            ArgumentErrorKind::Blank => formatter.write_str(" must not be blank"),
137            ArgumentErrorKind::Empty => formatter.write_str(" must not be empty"),
138            ArgumentErrorKind::Length {
139                actual,
140                constraint,
141                metric,
142            } => {
143                let label = length_metric_label(metric);
144                write!(formatter, " has {label} {actual}, expected ")?;
145                write_length_constraint(formatter, constraint)
146            }
147            ArgumentErrorKind::Comparison { actual, constraint } => {
148                write!(formatter, " has value {actual}, expected ")?;
149                write_comparison_constraint(formatter, constraint)
150            }
151            ArgumentErrorKind::Range { actual, constraint } => {
152                write!(formatter, " has value {actual}, expected range ")?;
153                write_range_constraint(formatter, constraint)
154            }
155            ArgumentErrorKind::InvalidLengthConstraint { constraint, metric } => {
156                let label = length_metric_label(metric);
157                write!(formatter, " has invalid {label} constraint ")?;
158                write_length_constraint(formatter, constraint)
159            }
160            ArgumentErrorKind::InvalidRangeConstraint { constraint } => {
161                formatter.write_str(" has invalid range constraint ")?;
162                write_range_constraint(formatter, constraint)
163            }
164            ArgumentErrorKind::NotANumber => formatter.write_str(" contains a NaN value"),
165            ArgumentErrorKind::NotFinite { actual } => {
166                write!(formatter, " has non-finite value {actual}")
167            }
168            ArgumentErrorKind::Index { index, size, role } => match role {
169                IndexRole::Element => write!(
170                    formatter,
171                    " has element index {index} outside the valid range 0..{size}",
172                ),
173                IndexRole::Position => write!(
174                    formatter,
175                    " has position index {index} outside the valid range 0..={size}",
176                ),
177            },
178            ArgumentErrorKind::IndexRange { start, end, size } => write!(
179                formatter,
180                " has position range {start}..{end} outside the valid range 0..={size}",
181            ),
182            ArgumentErrorKind::Bounds {
183                offset,
184                length,
185                total_length,
186            } => write!(
187                formatter,
188                " has offset {offset} and length {length} outside total length {total_length}",
189            ),
190            ArgumentErrorKind::Pattern { pattern, expectation } => match expectation {
191                PatternExpectation::Match => {
192                    let pattern = escape_for_display(pattern, Some('\''));
193                    write!(formatter, " must match pattern '{pattern}'")
194                }
195                PatternExpectation::NoMatch => {
196                    let pattern = escape_for_display(pattern, Some('\''));
197                    write!(formatter, " must not match pattern '{pattern}'")
198                }
199            },
200            ArgumentErrorKind::Custom { code, message } => {
201                let code = escape_for_display(code, Some(']'));
202                let message = escape_for_display(message, None);
203                write!(formatter, " failed validation [{code}]: {message}")
204            }
205        }
206    }
207}
208
209impl std::error::Error for ArgumentError {}
210
211/// Escapes caller-provided text for a single-line diagnostic.
212///
213/// The returned string represents `value` with backslashes, carriage returns,
214/// line feeds, tabs, other control characters, and the optional active
215/// `delimiter` escaped. The input remains unchanged.
216fn escape_for_display(value: &str, delimiter: Option<char>) -> String {
217    let mut escaped = String::with_capacity(value.len());
218    for character in value.chars() {
219        match character {
220            '\\' => escaped.push_str("\\\\"),
221            '\r' => escaped.push_str("\\r"),
222            '\n' => escaped.push_str("\\n"),
223            '\t' => escaped.push_str("\\t"),
224            character if Some(character) == delimiter => {
225                escaped.push('\\');
226                escaped.push(character);
227            }
228            character if character.is_control() => {
229                escaped.extend(character.escape_unicode());
230            }
231            character => escaped.push(character),
232        }
233    }
234    escaped
235}
236
237/// Returns the human-readable unit label for a measured length.
238///
239/// `metric` determines whether diagnostics describe UTF-8 byte length,
240/// Unicode scalar count, or collection element count.
241#[inline]
242fn length_metric_label(metric: &LengthMetric) -> &'static str {
243    match metric {
244        LengthMetric::Bytes => "byte length",
245        LengthMetric::UnicodeScalars => "Unicode scalar count",
246        LengthMetric::Elements => "element count",
247    }
248}
249
250/// Writes a length constraint in human-readable form.
251///
252/// `formatter` receives only text derived from `constraint`. Formatting errors
253/// from the destination are returned unchanged.
254fn write_length_constraint(formatter: &mut Formatter<'_>, constraint: &LengthConstraint) -> fmt::Result {
255    match constraint {
256        LengthConstraint::Exact(expected) => {
257            write!(formatter, "exactly {expected}")
258        }
259        LengthConstraint::AtLeast(min) => write!(formatter, "at least {min}"),
260        LengthConstraint::AtMost(max) => write!(formatter, "at most {max}"),
261        LengthConstraint::InRange { min, max } => {
262            write!(formatter, "between {min} and {max}")
263        }
264    }
265}
266
267/// Writes a scalar comparison constraint in human-readable form.
268///
269/// `formatter` receives only text derived from `constraint`. Formatting errors
270/// from the destination are returned unchanged.
271fn write_comparison_constraint(formatter: &mut Formatter<'_>, constraint: &ComparisonConstraint) -> fmt::Result {
272    match constraint {
273        ComparisonConstraint::EqualTo(expected) => {
274            write!(formatter, "equal to {expected}")
275        }
276        ComparisonConstraint::NotEqualTo(expected) => {
277            write!(formatter, "not equal to {expected}")
278        }
279        ComparisonConstraint::LessThan(bound) => {
280            write!(formatter, "less than {bound}")
281        }
282        ComparisonConstraint::AtMost(bound) => {
283            write!(formatter, "at most {bound}")
284        }
285        ComparisonConstraint::GreaterThan(bound) => {
286            write!(formatter, "greater than {bound}")
287        }
288        ComparisonConstraint::AtLeast(bound) => {
289            write!(formatter, "at least {bound}")
290        }
291    }
292}
293
294/// Writes a numeric range with notation that preserves both bound kinds.
295///
296/// `formatter` receives only text derived from `constraint`. Formatting errors
297/// from the destination are returned unchanged.
298fn write_range_constraint(formatter: &mut Formatter<'_>, constraint: &RangeConstraint) -> fmt::Result {
299    match constraint.lower() {
300        ArgumentBound::Unbounded => formatter.write_str("(-infinity"),
301        ArgumentBound::Included(value) => write!(formatter, "[{value}"),
302        ArgumentBound::Excluded(value) => write!(formatter, "({value}"),
303    }?;
304    formatter.write_str(", ")?;
305    match constraint.upper() {
306        ArgumentBound::Unbounded => formatter.write_str("infinity)"),
307        ArgumentBound::Included(value) => write!(formatter, "{value}]"),
308        ArgumentBound::Excluded(value) => write!(formatter, "{value})"),
309    }
310}
311
312/// The result of validating an argument, carrying [`ArgumentError`] on failure.
313///
314/// # Type Parameters
315///
316/// - `T`: The value produced when argument validation succeeds.
317pub type ArgumentResult<T> = Result<T, ArgumentError>;