qubit_http/options/http_config_error.rs
1/*******************************************************************************
2 *
3 * Copyright (c) 2025 - 2026.
4 * Haixing Hu, Qubit Co. Ltd.
5 *
6 * All rights reserved.
7 *
8 ******************************************************************************/
9//! # HTTP configuration error
10//!
11//! Error type for configuration-to-options conversion failures.
12//!
13//! # Author
14//!
15//! Haixing Hu
16
17use std::fmt;
18
19use super::HttpConfigErrorKind;
20
21/// Error type for HTTP configuration conversion failures.
22///
23/// Carries the failing configuration path and a human-readable message so that
24/// callers can report exactly which key caused the problem.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct HttpConfigError {
27 /// The configuration path that triggered the error, e.g. `http.proxy.port`.
28 pub path: String,
29 /// Human-readable description of the problem.
30 pub message: String,
31 /// Error category.
32 pub kind: HttpConfigErrorKind,
33}
34
35impl HttpConfigError {
36 /// Builds a configuration error with the given classification and message.
37 ///
38 /// # Parameters
39 /// - `kind`: Error category.
40 /// - `path`: Configuration key path (e.g. `http.proxy.port`).
41 /// - `message`: Human-readable explanation.
42 ///
43 /// # Returns
44 /// New [`HttpConfigError`].
45 pub fn new(
46 kind: HttpConfigErrorKind,
47 path: impl Into<String>,
48 message: impl Into<String>,
49 ) -> Self {
50 Self {
51 kind,
52 path: path.into(),
53 message: message.into(),
54 }
55 }
56
57 /// Shorthand for [`HttpConfigErrorKind::MissingField`].
58 ///
59 /// # Parameters
60 /// - `path`: Configuration path of the missing field.
61 /// - `message`: Explanation of what is missing.
62 ///
63 /// # Returns
64 /// New [`HttpConfigError`].
65 pub fn missing(path: impl Into<String>, message: impl Into<String>) -> Self {
66 Self::new(HttpConfigErrorKind::MissingField, path, message)
67 }
68
69 /// Shorthand for [`HttpConfigErrorKind::TypeError`].
70 ///
71 /// # Parameters
72 /// - `path`: Configuration path where the type mismatch occurred.
73 /// - `message`: Details of the expected vs actual type.
74 ///
75 /// # Returns
76 /// New [`HttpConfigError`].
77 pub fn type_error(path: impl Into<String>, message: impl Into<String>) -> Self {
78 Self::new(HttpConfigErrorKind::TypeError, path, message)
79 }
80
81 /// Shorthand for [`HttpConfigErrorKind::InvalidValue`].
82 ///
83 /// # Parameters
84 /// - `path`: Configuration path of the invalid value.
85 /// - `message`: Why the value is not acceptable.
86 ///
87 /// # Returns
88 /// New [`HttpConfigError`].
89 pub fn invalid_value(path: impl Into<String>, message: impl Into<String>) -> Self {
90 Self::new(HttpConfigErrorKind::InvalidValue, path, message)
91 }
92
93 /// Shorthand for [`HttpConfigErrorKind::InvalidHeader`].
94 ///
95 /// # Parameters
96 /// - `path`: Configuration path related to the header map entry.
97 /// - `message`: Header name/value problem description.
98 ///
99 /// # Returns
100 /// New [`HttpConfigError`].
101 pub fn invalid_header(path: impl Into<String>, message: impl Into<String>) -> Self {
102 Self::new(HttpConfigErrorKind::InvalidHeader, path, message)
103 }
104
105 /// Shorthand for [`HttpConfigErrorKind::ConfigError`] (underlying `qubit-config` failure).
106 ///
107 /// # Parameters
108 /// - `path`: Configuration path if known; may be empty when not applicable.
109 /// - `message`: Error text from the config layer.
110 ///
111 /// # Returns
112 /// New [`HttpConfigError`].
113 pub fn config_error(path: impl Into<String>, message: impl Into<String>) -> Self {
114 Self::new(HttpConfigErrorKind::ConfigError, path, message)
115 }
116
117 /// Prepends `prefix` to [`Self::path`] (for composing subsection parsers under a logical key).
118 ///
119 /// # Parameters
120 /// - `prefix`: Segment such as `timeouts` or `proxy`; empty leaves `self` unchanged.
121 ///
122 /// # Returns
123 /// Updated error with `path` = `prefix` or `{prefix}.{path}`.
124 pub(crate) fn prepend_path_prefix(mut self, prefix: &str) -> Self {
125 let prefix_with_dot = format!("{prefix}.");
126 let already_prefixed = self.path == prefix || self.path.starts_with(&prefix_with_dot);
127 if !already_prefixed {
128 self.path = self
129 .path
130 .find(&prefix_with_dot)
131 .map(|index| self.path[index..].to_string())
132 .unwrap_or_else(|| {
133 [prefix, self.path.as_str()]
134 .into_iter()
135 .filter(|part| !part.is_empty())
136 .collect::<Vec<_>>()
137 .join(".")
138 });
139 }
140 self
141 }
142}
143
144impl fmt::Display for HttpConfigError {
145 /// Formats as `[kind] path: message`.
146 ///
147 /// # Parameters
148 /// - `f`: Destination formatter.
149 ///
150 /// # Returns
151 /// [`fmt::Result`].
152 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153 write!(f, "[{}] {}: {}", self.kind, self.path, self.message)
154 }
155}
156
157impl std::error::Error for HttpConfigError {}
158
159impl From<qubit_config::ConfigError> for HttpConfigError {
160 /// Converts a `qubit_config::ConfigError`, mapping typed failures to
161 /// [`HttpConfigErrorKind::TypeError`] when the source carries a property key.
162 ///
163 /// # Parameters
164 /// - `e`: Source configuration error.
165 ///
166 /// # Returns
167 /// Equivalent [`HttpConfigError`].
168 fn from(e: qubit_config::ConfigError) -> Self {
169 use qubit_config::ConfigError;
170 let msg = e.to_string();
171 match e {
172 ConfigError::TypeMismatch { key, .. } | ConfigError::ConversionError { key, .. } => {
173 HttpConfigError::type_error(key, msg)
174 }
175 ConfigError::PropertyHasNoValue(key) => HttpConfigError::type_error(key, msg),
176 ConfigError::PropertyNotFound(key) => HttpConfigError::config_error(key, msg),
177 other => HttpConfigError::config_error("", other.to_string()),
178 }
179 }
180}