qubit_retry/error/retry_config_error.rs
1/*******************************************************************************
2 *
3 * Copyright (c) 2025 - 2026 Haixing Hu.
4 *
5 * SPDX-License-Identifier: Apache-2.0
6 *
7 * Licensed under the Apache License, Version 2.0.
8 *
9 ******************************************************************************/
10//! Configuration validation errors.
11//!
12//! This module keeps retry configuration failures independent from executor
13//! execution failures so callers can distinguish setup errors from operation
14//! errors.
15
16use std::error::Error;
17use std::fmt;
18
19#[cfg(feature = "config")]
20use qubit_config::ConfigError;
21
22/// Invalid retry configuration.
23///
24/// `path` stores the configuration key that failed when such context is
25/// available. `message` stores the human-readable reason.
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct RetryConfigError {
28 path: String,
29 message: String,
30}
31
32impl RetryConfigError {
33 /// Creates a validation error for a retry option.
34 ///
35 /// # Parameters
36 /// - `path`: Configuration key or option name associated with the failure.
37 /// - `message`: Human-readable validation message.
38 ///
39 /// # Returns
40 /// A new [`RetryConfigError`].
41 ///
42 /// # Errors
43 /// This function does not return errors.
44 #[inline]
45 pub fn invalid_value(path: impl Into<String>, message: impl Into<String>) -> Self {
46 Self {
47 path: path.into(),
48 message: message.into(),
49 }
50 }
51
52 /// Wraps an error returned by `qubit-config`.
53 ///
54 /// # Parameters
55 /// - `path`: Configuration key that was being read.
56 /// - `source`: Error returned by `qubit-config`.
57 ///
58 /// # Returns
59 /// A new [`RetryConfigError`] that preserves the key and source message.
60 ///
61 /// # Errors
62 /// This function does not return errors.
63 #[inline]
64 #[cfg(feature = "config")]
65 pub fn from_config(path: impl Into<String>, source: ConfigError) -> Self {
66 Self {
67 path: path.into(),
68 message: source.to_string(),
69 }
70 }
71
72 /// Returns the configuration path associated with this error.
73 ///
74 /// # Parameters
75 /// This method has no parameters.
76 ///
77 /// # Returns
78 /// The configuration path, or an empty string when the error was not tied
79 /// to a specific key.
80 ///
81 /// # Errors
82 /// This method does not return errors.
83 #[inline]
84 pub fn path(&self) -> &str {
85 &self.path
86 }
87
88 /// Returns the error message.
89 ///
90 /// # Parameters
91 /// This method has no parameters.
92 ///
93 /// # Returns
94 /// The human-readable validation or configuration read message.
95 ///
96 /// # Errors
97 /// This method does not return errors.
98 #[inline]
99 pub fn message(&self) -> &str {
100 &self.message
101 }
102}
103
104impl fmt::Display for RetryConfigError {
105 /// Formats the configuration error for diagnostics.
106 ///
107 /// # Parameters
108 /// - `f`: Formatter provided by the standard formatting machinery.
109 ///
110 /// # Returns
111 /// `fmt::Result` from the formatter.
112 ///
113 /// # Errors
114 /// Returns a formatting error if the underlying formatter fails.
115 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116 if self.path.is_empty() {
117 write!(f, "invalid retry configuration: {}", self.message)
118 } else {
119 write!(
120 f,
121 "invalid retry configuration at '{}': {}",
122 self.path, self.message
123 )
124 }
125 }
126}
127
128impl Error for RetryConfigError {}
129
130#[cfg(feature = "config")]
131impl From<ConfigError> for RetryConfigError {
132 /// Converts a `qubit-config` error into a retry configuration error.
133 ///
134 /// # Parameters
135 /// - `source`: Error returned by `qubit-config`.
136 ///
137 /// # Returns
138 /// A [`RetryConfigError`] with the path carried by `source` when
139 /// available, or an empty path for config errors that do not include key
140 /// context.
141 ///
142 /// # Errors
143 /// This function does not return errors.
144 #[inline]
145 fn from(source: ConfigError) -> Self {
146 let path = match &source {
147 ConfigError::PropertyNotFound(path)
148 | ConfigError::PropertyHasNoValue(path)
149 | ConfigError::PropertyIsFinal(path) => path.clone(),
150 ConfigError::TypeMismatch { key, .. } | ConfigError::ConversionError { key, .. } => {
151 key.clone()
152 }
153 ConfigError::DeserializeError { path, .. } => path.clone(),
154 ConfigError::KeyConflict { path, .. } => path.clone(),
155 ConfigError::IndexOutOfBounds { .. }
156 | ConfigError::SubstitutionError(_)
157 | ConfigError::SubstitutionDepthExceeded(_)
158 | ConfigError::SubstitutionCycle { .. }
159 | ConfigError::MergeError(_)
160 | ConfigError::IoError(_)
161 | ConfigError::ParseError(_)
162 | ConfigError::Other(_) => String::new(),
163 };
164 Self::from_config(path, source)
165 }
166}