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