pub type Result<T> = std::result::Result<T, LeashError>;
#[derive(Debug, thiserror::Error)]
pub enum LeashError {
#[error("plan block: {message}")]
PlanBlock {
code: String,
message: String,
required_plan: Option<String>,
},
#[error("connection required for {provider}: {message}")]
ConnectionRequired {
provider: String,
message: String,
connect_url: Option<String>,
},
#[error("upgrade required: {message}")]
UpgradeRequired {
message: String,
},
#[error("env key '{key}' is not declared")]
KeyNotDeclared {
key: String,
},
#[error("unauthorized: {message}")]
Unauthorized {
message: String,
},
#[error("network error: {0}")]
Network(#[from] reqwest::Error),
#[error("upstream error (HTTP {status}): {message}")]
UpstreamError {
status: u16,
message: String,
},
#[error("malformed response: {message}")]
MalformedResponse {
message: String,
},
}
impl LeashError {
pub fn is_plan_block(&self) -> bool {
matches!(self, Self::PlanBlock { .. } | Self::UpgradeRequired { .. })
}
pub fn is_upgrade_required(&self) -> bool {
self.is_plan_block()
}
pub fn is_connection_required(&self) -> bool {
matches!(self, Self::ConnectionRequired { .. })
}
pub fn is_unauthorized(&self) -> bool {
matches!(self, Self::Unauthorized { .. })
}
pub fn is_key_not_declared(&self) -> bool {
matches!(self, Self::KeyNotDeclared { .. })
}
pub fn is_network(&self) -> bool {
matches!(self, Self::Network(_))
}
pub fn status(&self) -> Option<u16> {
match self {
Self::PlanBlock { .. } => Some(402),
Self::ConnectionRequired { .. } => Some(403),
Self::UpgradeRequired { .. } => Some(402),
Self::Unauthorized { .. } => Some(401),
Self::UpstreamError { status, .. } => Some(*status),
Self::Network(e) => e.status().map(|s| s.as_u16()),
Self::KeyNotDeclared { .. } | Self::MalformedResponse { .. } => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plan_block_predicate() {
let err = LeashError::PlanBlock {
code: "UPGRADE_REQUIRED".into(),
message: "Growth required".into(),
required_plan: Some("growth".into()),
};
assert!(err.is_plan_block());
assert!(err.is_upgrade_required());
assert!(!err.is_connection_required());
assert_eq!(err.status(), Some(402));
}
#[test]
fn connection_required_predicate() {
let err = LeashError::ConnectionRequired {
provider: "gmail".into(),
message: "not connected".into(),
connect_url: Some("https://leash.build/connect/gmail".into()),
};
assert!(err.is_connection_required());
assert!(!err.is_plan_block());
assert_eq!(err.status(), Some(403));
}
#[test]
fn unauthorized_predicate() {
let err = LeashError::Unauthorized {
message: "nope".into(),
};
assert!(err.is_unauthorized());
assert_eq!(err.status(), Some(401));
}
#[test]
fn key_not_declared_predicate() {
let err = LeashError::KeyNotDeclared {
key: "OPENAI_API_KEY".into(),
};
assert!(err.is_key_not_declared());
assert_eq!(err.status(), None);
}
#[test]
fn upstream_error_carries_status() {
let err = LeashError::UpstreamError {
status: 500,
message: "boom".into(),
};
assert_eq!(err.status(), Some(500));
assert!(!err.is_plan_block());
}
#[test]
fn malformed_response_has_no_status() {
let err = LeashError::MalformedResponse {
message: "missing field".into(),
};
assert_eq!(err.status(), None);
}
}