use std::{fmt, time::SystemTime, time::SystemTimeError};
use thiserror::Error as ThisError;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ErrorKind {
Api,
Transport,
Stream,
Serialization,
Signature,
Timestamp,
InvalidConfig,
InvalidInput,
MissingCredentials,
BotScope,
}
impl ErrorKind {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Api => "api",
Self::Transport => "transport",
Self::Stream => "stream",
Self::Serialization => "serialization",
Self::Signature => "signature",
Self::Timestamp => "timestamp",
Self::InvalidConfig => "invalid_config",
Self::InvalidInput => "invalid_input",
Self::MissingCredentials => "missing_credentials",
Self::BotScope => "bot_scope",
}
}
}
#[derive(Debug, ThisError)]
#[non_exhaustive]
pub enum Error {
#[error("DingTalk API error (code={code}): {message}")]
Api {
code: i64,
message: String,
request_id: Option<String>,
error_body_snippet: Option<String>,
},
#[error("HTTP transport error: {0}")]
Transport(#[from] reqx::Error),
#[error("stream error: {0}")]
Stream(String),
#[error("serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("timestamp generation failed: {0}")]
Timestamp(#[from] SystemTimeError),
#[error("signature generation failed")]
Signature,
#[error("signature verification failed: {0}")]
InvalidSignature(String),
#[error("invalid configuration: {0}")]
InvalidConfig(String),
#[error("invalid input `{field}`: {message}")]
InvalidInput {
field: &'static str,
message: String,
},
#[error("app credentials are required for this operation")]
MissingCredentials,
#[error("bot context scope mismatch: expected {expected}, got {actual}")]
BotScope {
expected: &'static str,
actual: String,
},
}
impl Error {
#[must_use]
pub fn kind(&self) -> ErrorKind {
match self {
Self::Api { .. } => ErrorKind::Api,
Self::Transport(_) => ErrorKind::Transport,
Self::Stream(_) => ErrorKind::Stream,
Self::Serialization(_) => ErrorKind::Serialization,
Self::Signature | Self::InvalidSignature(_) => ErrorKind::Signature,
Self::Timestamp(_) => ErrorKind::Timestamp,
Self::InvalidConfig(_) => ErrorKind::InvalidConfig,
Self::InvalidInput { .. } => ErrorKind::InvalidInput,
Self::MissingCredentials => ErrorKind::MissingCredentials,
Self::BotScope { .. } => ErrorKind::BotScope,
}
}
#[must_use]
pub fn request_id(&self) -> Option<&str> {
match self {
Self::Api { request_id, .. } => request_id.as_deref(),
Self::Transport(error) => error.request_id(),
_ => None,
}
}
#[must_use]
pub fn error_body_snippet(&self) -> Option<&str> {
match self {
Self::Api {
error_body_snippet, ..
} => error_body_snippet.as_deref(),
_ => None,
}
}
#[must_use]
pub fn status(&self) -> Option<u16> {
match self {
Self::Transport(error) => error.status_code(),
_ => None,
}
}
#[must_use]
pub fn is_retryable(&self) -> bool {
match self {
Self::Transport(error) => match error.code() {
reqx::ErrorCode::Timeout
| reqx::ErrorCode::DeadlineExceeded
| reqx::ErrorCode::Transport
| reqx::ErrorCode::RetryBudgetExhausted
| reqx::ErrorCode::CircuitOpen => true,
reqx::ErrorCode::HttpStatus => {
matches!(error.status_code(), Some(429 | 500..=599))
}
_ => false,
},
Self::Api { code, .. } => matches!(*code, 130101 | 130102),
_ => false,
}
}
#[must_use]
pub fn retry_after(&self) -> Option<std::time::Duration> {
match self {
Self::Transport(error) => error.retry_after(SystemTime::now()),
_ => None,
}
}
pub(crate) fn invalid_input(field: &'static str, message: impl Into<String>) -> Self {
Self::InvalidInput {
field,
message: message.into(),
}
}
#[cfg(feature = "bot")]
pub(crate) fn invalid_signature(message: impl Into<String>) -> Self {
Self::InvalidSignature(message.into())
}
pub(crate) fn api(
code: i64,
message: impl Into<String>,
request_id: Option<String>,
error_body_snippet: Option<String>,
) -> Self {
Self::Api {
code,
message: message.into(),
request_id,
error_body_snippet,
}
}
}
impl fmt::Display for ErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn error_kind_exposes_stable_labels() {
assert_eq!(ErrorKind::Api.as_str(), "api");
assert_eq!(ErrorKind::Transport.to_string(), "transport");
assert_eq!(ErrorKind::InvalidConfig.as_str(), "invalid_config");
assert_eq!(
ErrorKind::MissingCredentials.to_string(),
"missing_credentials"
);
}
}