Skip to main content

kibana_sync/
error.rs

1use reqwest::StatusCode;
2use std::fmt;
3
4/// Result type used by `kibana-sync` public APIs.
5pub type Result<T> = std::result::Result<T, Error>;
6
7pub trait ResultContext<T> {
8    fn context(self, message: impl Into<String>) -> Result<T>;
9    fn with_context<M>(self, message: impl FnOnce() -> M) -> Result<T>
10    where
11        M: Into<String>;
12}
13
14impl<T, E> ResultContext<T> for std::result::Result<T, E>
15where
16    E: Into<Error>,
17{
18    fn context(self, message: impl Into<String>) -> Result<T> {
19        self.map_err(|err| Error::context(message, err.into()))
20    }
21
22    fn with_context<M>(self, message: impl FnOnce() -> M) -> Result<T>
23    where
24        M: Into<String>,
25    {
26        self.map_err(|err| Error::context(message(), err.into()))
27    }
28}
29
30/// Error type used by `kibana-sync` public APIs.
31#[derive(Debug)]
32#[non_exhaustive]
33pub enum Error {
34    InvalidConfiguration(String),
35    InvalidSpace {
36        id: String,
37        available: Vec<String>,
38    },
39    UnsupportedCapability {
40        capability: &'static str,
41        detected: semver::Version,
42        minimum: semver::Version,
43    },
44    MissingResourceId {
45        resource: &'static str,
46    },
47    MissingField {
48        field: &'static str,
49    },
50    RedactedStatusUnauthenticated,
51    ApiResponse {
52        status: StatusCode,
53        body: String,
54    },
55    Transport(reqwest::Error),
56    Url(url::ParseError),
57    HeaderName(reqwest::header::InvalidHeaderName),
58    HeaderValue(reqwest::header::InvalidHeaderValue),
59    HeaderToStr(reqwest::header::ToStrError),
60    Json(serde_json::Error),
61    Yaml(yaml_serde::Error),
62    Version(semver::Error),
63    Io(std::io::Error),
64    SemaphoreClosed,
65    Message(String),
66    Context {
67        message: String,
68        source: Box<Error>,
69    },
70}
71
72impl Error {
73    pub fn api_response(status: StatusCode, body: impl Into<String>) -> Self {
74        Self::ApiResponse {
75            status,
76            body: body.into(),
77        }
78    }
79
80    pub fn message(message: impl Into<String>) -> Self {
81        Self::Message(message.into())
82    }
83
84    pub fn context(message: impl Into<String>, source: Error) -> Self {
85        Self::Context {
86            message: message.into(),
87            source: Box::new(source),
88        }
89    }
90}
91
92impl fmt::Display for Error {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        match self {
95            Self::InvalidConfiguration(message) => write!(f, "invalid configuration: {message}"),
96            Self::InvalidSpace { id, available } => write!(
97                f,
98                "space '{id}' not found in registry. Available spaces: {}",
99                available.join(", ")
100            ),
101            Self::UnsupportedCapability {
102                capability,
103                detected,
104                minimum,
105            } => write!(
106                f,
107                "API '{capability}' requires Kibana {minimum}+ (detected {detected})"
108            ),
109            Self::MissingResourceId { resource } => {
110                write!(f, "{resource} is missing required 'id' field")
111            }
112            Self::MissingField { field } => write!(f, "missing required field: {field}"),
113            Self::RedactedStatusUnauthenticated => write!(
114                f,
115                "Kibana /api/status response did not include version.number; this usually means Kibana returned the unauthenticated redacted status response. Check KIBANA_APIKEY or KIBANA_USERNAME/KIBANA_PASSWORD credentials"
116            ),
117            Self::ApiResponse { status, body } => {
118                write!(f, "Kibana API returned {status}: {body}")
119            }
120            Self::Transport(err) => write!(f, "transport error: {err}"),
121            Self::Url(err) => write!(f, "URL error: {err}"),
122            Self::HeaderName(err) => write!(f, "invalid header name: {err}"),
123            Self::HeaderValue(err) => write!(f, "invalid header value: {err}"),
124            Self::HeaderToStr(err) => write!(f, "invalid header value: {err}"),
125            Self::Json(err) => write!(f, "JSON error: {err}"),
126            Self::Yaml(err) => write!(f, "YAML error: {err}"),
127            Self::Version(err) => write!(f, "version parse error: {err}"),
128            Self::Io(err) => write!(f, "I/O error: {err}"),
129            Self::SemaphoreClosed => write!(f, "request semaphore closed"),
130            Self::Message(message) => f.write_str(message),
131            Self::Context { message, source } => write!(f, "{message}: {source}"),
132        }
133    }
134}
135
136impl std::error::Error for Error {
137    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
138        match self {
139            Self::Transport(err) => Some(err),
140            Self::Url(err) => Some(err),
141            Self::HeaderName(err) => Some(err),
142            Self::HeaderValue(err) => Some(err),
143            Self::HeaderToStr(err) => Some(err),
144            Self::Json(err) => Some(err),
145            Self::Yaml(err) => Some(err),
146            Self::Version(err) => Some(err),
147            Self::Io(err) => Some(err),
148            Self::Context { source, .. } => Some(source.as_ref()),
149            _ => None,
150        }
151    }
152}
153
154impl From<reqwest::Error> for Error {
155    fn from(err: reqwest::Error) -> Self {
156        Self::Transport(err)
157    }
158}
159
160impl From<url::ParseError> for Error {
161    fn from(err: url::ParseError) -> Self {
162        Self::Url(err)
163    }
164}
165
166impl From<reqwest::header::InvalidHeaderName> for Error {
167    fn from(err: reqwest::header::InvalidHeaderName) -> Self {
168        Self::HeaderName(err)
169    }
170}
171
172impl From<reqwest::header::InvalidHeaderValue> for Error {
173    fn from(err: reqwest::header::InvalidHeaderValue) -> Self {
174        Self::HeaderValue(err)
175    }
176}
177
178impl From<reqwest::header::ToStrError> for Error {
179    fn from(err: reqwest::header::ToStrError) -> Self {
180        Self::HeaderToStr(err)
181    }
182}
183
184impl From<serde_json::Error> for Error {
185    fn from(err: serde_json::Error) -> Self {
186        Self::Json(err)
187    }
188}
189
190impl From<yaml_serde::Error> for Error {
191    fn from(err: yaml_serde::Error) -> Self {
192        Self::Yaml(err)
193    }
194}
195
196impl From<semver::Error> for Error {
197    fn from(err: semver::Error) -> Self {
198        Self::Version(err)
199    }
200}
201
202impl From<std::io::Error> for Error {
203    fn from(err: std::io::Error) -> Self {
204        Self::Io(err)
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use std::error::Error as _;
212
213    #[test]
214    fn result_context_preserves_error_source() {
215        let result: std::result::Result<(), std::io::Error> =
216            Err(std::io::Error::other("disk full"));
217
218        let err = result.context("failed to read manifest").unwrap_err();
219
220        assert!(matches!(&err, Error::Context { .. }));
221        assert!(matches!(err.source(), Some(source) if source.is::<Error>()));
222    }
223}