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!(f, "invalid retry configuration at '{}': {}", self.path, self.message)
120 }
121 }
122}
123
124impl Error for RetryConfigError {}
125
126#[cfg(feature = "config")]
127impl From<ConfigError> for RetryConfigError {
128 /// Converts a `qubit-config` error into a retry configuration error.
129 ///
130 /// # Parameters
131 /// - `source`: Error returned by `qubit-config`.
132 ///
133 /// # Returns
134 /// A [`RetryConfigError`] with the path carried by `source` when
135 /// available, or an empty path for config errors that do not include key
136 /// context.
137 ///
138 /// # Errors
139 /// This function does not return errors.
140 #[inline]
141 fn from(source: ConfigError) -> Self {
142 let path = match &source {
143 ConfigError::PropertyNotFound(path)
144 | ConfigError::PropertyHasNoValue(path)
145 | ConfigError::PropertyIsFinal(path) => path.clone(),
146 ConfigError::TypeMismatch { key, .. } | ConfigError::ConversionError { key, .. } => key.clone(),
147 ConfigError::DeserializeError { path, .. } => path.clone(),
148 ConfigError::KeyConflict { path, .. } => path.clone(),
149 ConfigError::IndexOutOfBounds { .. }
150 | ConfigError::SubstitutionError(_)
151 | ConfigError::SubstitutionDepthExceeded(_)
152 | ConfigError::SubstitutionCycle { .. }
153 | ConfigError::MergeError(_)
154 | ConfigError::IoError(_)
155 | ConfigError::ParseError(_)
156 | ConfigError::Other(_) => String::new(),
157 };
158 Self::from_config(path, source)
159 }
160}