1use 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#[must_use]
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct ArgumentError {
49 path: ArgumentPath,
51 kind: Box<ArgumentErrorKind>,
53}
54
55impl ArgumentError {
56 #[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 #[must_use = "the caller should inspect the argument path"]
83 #[inline]
84 pub fn path(&self) -> &ArgumentPath {
85 &self.path
86 }
87
88 #[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 #[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 #[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 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
211fn 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#[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
250fn 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
267fn 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
294fn 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
312pub type ArgumentResult<T> = Result<T, ArgumentError>;