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