use thiserror::Error;
pub type Result<T, E = ClientError> = std::result::Result<T, E>;
#[derive(Debug, Error)]
pub enum ClientError {
#[error("{command}: transport error: {source}")]
Transport {
command: String,
#[source]
source: Box<ureq::Error>,
},
#[error("{command}: cluster error {code}: {message}")]
Cluster {
command: String,
code: i64,
message: String,
raw: String,
},
#[error("{command}: unexpected HTTP {status}{}", body_hint(.body))]
Http {
command: String,
status: u16,
body: String,
},
#[error("{command}: could not decode the response: {reason}")]
Decode {
command: String,
reason: String,
},
#[error("reading {path}: {source}")]
Io {
path: String,
#[source]
source: std::io::Error,
},
#[error("operation {id} finished as {state}{}", failure_hint(.error))]
OperationFailed {
id: String,
state: String,
error: Option<String>,
},
#[error("{0}")]
Config(String),
}
fn body_hint(body: &str) -> String {
if body.trim().is_empty() {
String::new()
} else {
format!(": {}", body.trim())
}
}
fn failure_hint(error: &Option<String>) -> String {
match error {
Some(e) if !e.trim().is_empty() => format!(": {}", e.trim()),
_ => String::new(),
}
}
impl ClientError {
pub(crate) fn from_yt_error(command: &str, status: u16, raw: &str) -> Self {
let parsed: Option<serde_json::Value> = serde_json::from_str(raw).ok();
match parsed {
Some(value) => {
let code = value
.get("code")
.and_then(serde_json::Value::as_i64)
.unwrap_or(-1);
let message = value
.get("message")
.and_then(serde_json::Value::as_str)
.unwrap_or("(no message)")
.to_owned();
let message = match innermost_message(&value) {
Some(inner) if inner != message => format!("{message}: {inner}"),
_ => message,
};
ClientError::Cluster {
command: command.to_owned(),
code,
message,
raw: raw.to_owned(),
}
}
None => ClientError::Http {
command: command.to_owned(),
status,
body: truncate(raw, 400),
},
}
}
}
fn innermost_message(value: &serde_json::Value) -> Option<String> {
let inner = value.get("inner_errors")?.as_array()?;
let first = inner.first()?;
innermost_message(first).or_else(|| {
first
.get("message")
.and_then(serde_json::Value::as_str)
.map(str::to_owned)
})
}
pub(crate) fn truncate(s: &str, limit: usize) -> String {
if s.len() <= limit {
return s.to_owned();
}
let mut end = limit;
while end > 0 && !s.is_char_boundary(end) {
end -= 1;
}
format!("{}… ({} bytes total)", &s[..end], s.len())
}