use reqwest::StatusCode;
use std::fmt;
pub type Result<T> = std::result::Result<T, Error>;
pub trait ResultContext<T> {
fn context(self, message: impl Into<String>) -> Result<T>;
fn with_context<M>(self, message: impl FnOnce() -> M) -> Result<T>
where
M: Into<String>;
}
impl<T, E> ResultContext<T> for std::result::Result<T, E>
where
E: Into<Error>,
{
fn context(self, message: impl Into<String>) -> Result<T> {
self.map_err(|err| Error::context(message, err.into()))
}
fn with_context<M>(self, message: impl FnOnce() -> M) -> Result<T>
where
M: Into<String>,
{
self.map_err(|err| Error::context(message(), err.into()))
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
InvalidConfiguration(String),
InvalidSpace {
id: String,
available: Vec<String>,
},
UnsupportedCapability {
capability: &'static str,
detected: semver::Version,
minimum: semver::Version,
},
MissingResourceId {
resource: &'static str,
},
MissingField {
field: &'static str,
},
RedactedStatusUnauthenticated,
ApiResponse {
status: StatusCode,
body: String,
},
Transport(reqwest::Error),
Url(url::ParseError),
HeaderName(reqwest::header::InvalidHeaderName),
HeaderValue(reqwest::header::InvalidHeaderValue),
HeaderToStr(reqwest::header::ToStrError),
Json(serde_json::Error),
Yaml(yaml_serde::Error),
Version(semver::Error),
Io(std::io::Error),
SemaphoreClosed,
Message(String),
Context {
message: String,
source: Box<Error>,
},
}
impl Error {
pub fn api_response(status: StatusCode, body: impl Into<String>) -> Self {
Self::ApiResponse {
status,
body: body.into(),
}
}
pub fn message(message: impl Into<String>) -> Self {
Self::Message(message.into())
}
pub fn context(message: impl Into<String>, source: Error) -> Self {
Self::Context {
message: message.into(),
source: Box::new(source),
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidConfiguration(message) => write!(f, "invalid configuration: {message}"),
Self::InvalidSpace { id, available } => write!(
f,
"space '{id}' not found in registry. Available spaces: {}",
available.join(", ")
),
Self::UnsupportedCapability {
capability,
detected,
minimum,
} => write!(
f,
"API '{capability}' requires Kibana {minimum}+ (detected {detected})"
),
Self::MissingResourceId { resource } => {
write!(f, "{resource} is missing required 'id' field")
}
Self::MissingField { field } => write!(f, "missing required field: {field}"),
Self::RedactedStatusUnauthenticated => write!(
f,
"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"
),
Self::ApiResponse { status, body } => {
write!(f, "Kibana API returned {status}: {body}")
}
Self::Transport(err) => write!(f, "transport error: {err}"),
Self::Url(err) => write!(f, "URL error: {err}"),
Self::HeaderName(err) => write!(f, "invalid header name: {err}"),
Self::HeaderValue(err) => write!(f, "invalid header value: {err}"),
Self::HeaderToStr(err) => write!(f, "invalid header value: {err}"),
Self::Json(err) => write!(f, "JSON error: {err}"),
Self::Yaml(err) => write!(f, "YAML error: {err}"),
Self::Version(err) => write!(f, "version parse error: {err}"),
Self::Io(err) => write!(f, "I/O error: {err}"),
Self::SemaphoreClosed => write!(f, "request semaphore closed"),
Self::Message(message) => f.write_str(message),
Self::Context { message, source } => write!(f, "{message}: {source}"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Transport(err) => Some(err),
Self::Url(err) => Some(err),
Self::HeaderName(err) => Some(err),
Self::HeaderValue(err) => Some(err),
Self::HeaderToStr(err) => Some(err),
Self::Json(err) => Some(err),
Self::Yaml(err) => Some(err),
Self::Version(err) => Some(err),
Self::Io(err) => Some(err),
Self::Context { source, .. } => Some(source.as_ref()),
_ => None,
}
}
}
impl From<reqwest::Error> for Error {
fn from(err: reqwest::Error) -> Self {
Self::Transport(err)
}
}
impl From<url::ParseError> for Error {
fn from(err: url::ParseError) -> Self {
Self::Url(err)
}
}
impl From<reqwest::header::InvalidHeaderName> for Error {
fn from(err: reqwest::header::InvalidHeaderName) -> Self {
Self::HeaderName(err)
}
}
impl From<reqwest::header::InvalidHeaderValue> for Error {
fn from(err: reqwest::header::InvalidHeaderValue) -> Self {
Self::HeaderValue(err)
}
}
impl From<reqwest::header::ToStrError> for Error {
fn from(err: reqwest::header::ToStrError) -> Self {
Self::HeaderToStr(err)
}
}
impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Self {
Self::Json(err)
}
}
impl From<yaml_serde::Error> for Error {
fn from(err: yaml_serde::Error) -> Self {
Self::Yaml(err)
}
}
impl From<semver::Error> for Error {
fn from(err: semver::Error) -> Self {
Self::Version(err)
}
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Self {
Self::Io(err)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error as _;
#[test]
fn result_context_preserves_error_source() {
let result: std::result::Result<(), std::io::Error> =
Err(std::io::Error::other("disk full"));
let err = result.context("failed to read manifest").unwrap_err();
assert!(matches!(&err, Error::Context { .. }));
assert!(matches!(err.source(), Some(source) if source.is::<Error>()));
}
}