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