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