#![allow(dead_code)]
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ErrorCategory {
#[default]
Unknown,
Transient,
Permanent,
}
impl ErrorCategory {
pub fn as_str(&self) -> &'static str {
match self {
Self::Unknown => "unknown",
Self::Transient => "transient",
Self::Permanent => "permanent",
}
}
pub fn from_str(s: &str) -> Self {
match s {
"transient" => Self::Transient,
"permanent" => Self::Permanent,
"business" => Self::Permanent,
_ => Self::Unknown,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ErrorSeverity {
Info,
Warning,
#[default]
Error,
Critical,
}
impl ErrorSeverity {
pub fn as_str(&self) -> &'static str {
match self {
Self::Info => "info",
Self::Warning => "warning",
Self::Error => "error",
Self::Critical => "critical",
}
}
pub fn from_str(s: &str) -> Self {
match s {
"info" => Self::Info,
"warning" => Self::Warning,
"critical" => Self::Critical,
_ => Self::Error,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum RetryHint {
#[default]
Unknown,
RetryImmediately,
RetryWithBackoff,
#[serde(rename = "retry_after")]
RetryAfter(u64),
DoNotRetry,
}
impl RetryHint {
pub fn as_str(&self) -> &'static str {
match self {
Self::Unknown => "unknown",
Self::RetryImmediately => "retry_immediately",
Self::RetryWithBackoff => "retry_with_backoff",
Self::RetryAfter(_) => "retry_after",
Self::DoNotRetry => "do_not_retry",
}
}
pub fn retry_after_ms(&self) -> Option<u64> {
match self {
Self::RetryAfter(ms) => Some(*ms),
_ => None,
}
}
pub fn should_retry(&self) -> bool {
matches!(
self,
Self::RetryImmediately | Self::RetryWithBackoff | Self::RetryAfter(_)
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorInfo {
pub code: String,
pub message: String,
#[serde(default)]
pub category: ErrorCategory,
#[serde(default)]
pub severity: ErrorSeverity,
#[serde(default)]
pub retry_hint: RetryHint,
#[serde(skip_serializing_if = "Option::is_none")]
pub source_step_id: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub attributes: HashMap<String, String>,
}
impl ErrorInfo {
pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
Self {
code: code.into(),
message: message.into(),
category: ErrorCategory::Unknown,
severity: ErrorSeverity::Error,
retry_hint: RetryHint::Unknown,
source_step_id: None,
attributes: HashMap::new(),
}
}
pub fn transient(code: impl Into<String>, message: impl Into<String>) -> Self {
Self {
code: code.into(),
message: message.into(),
category: ErrorCategory::Transient,
severity: ErrorSeverity::Error,
retry_hint: RetryHint::RetryWithBackoff,
source_step_id: None,
attributes: HashMap::new(),
}
}
pub fn permanent(code: impl Into<String>, message: impl Into<String>) -> Self {
Self {
code: code.into(),
message: message.into(),
category: ErrorCategory::Permanent,
severity: ErrorSeverity::Error,
retry_hint: RetryHint::DoNotRetry,
source_step_id: None,
attributes: HashMap::new(),
}
}
pub fn with_step(mut self, step_id: impl Into<String>) -> Self {
self.source_step_id = Some(step_id.into());
self
}
pub fn with_attr(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.attributes.insert(key.into(), value.into());
self
}
pub fn with_severity(mut self, severity: ErrorSeverity) -> Self {
self.severity = severity;
self
}
pub fn is_transient(&self) -> bool {
self.category == ErrorCategory::Transient
}
pub fn is_permanent(&self) -> bool {
self.category == ErrorCategory::Permanent
}
pub fn should_retry(&self) -> bool {
self.retry_hint.should_retry()
}
}
impl std::fmt::Display for ErrorInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}] {}", self.code, self.message)
}
}
impl std::error::Error for ErrorInfo {}
#[derive(Debug, Error)]
pub enum SdkError {
#[error("configuration error: {0}")]
Config(String),
#[error("registration failed: {0}")]
Registration(String),
#[error("checkpoint error: {0}")]
Checkpoint(String),
#[error("sleep error: {0}")]
Sleep(String),
#[error("event error: {0}")]
Event(String),
#[error("signal error: {0}")]
Signal(String),
#[error("status error: {0}")]
Status(String),
#[error("server error: {code} - {message}")]
Server {
code: String,
message: String,
},
#[error("structured error: {0}")]
StructuredError(Box<ErrorInfo>),
#[error("instance cancelled")]
Cancelled,
#[error("instance paused")]
Paused,
#[error("serialization error: {0}")]
Serialization(String),
#[error("unexpected response: {0}")]
UnexpectedResponse(String),
#[error("internal error: {0}")]
Internal(String),
}
pub type Result<T> = std::result::Result<T, SdkError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_error_display() {
let err = SdkError::Config("missing RUNTARA_INSTANCE_ID".to_string());
assert_eq!(
format!("{}", err),
"configuration error: missing RUNTARA_INSTANCE_ID"
);
}
#[test]
fn test_registration_error_display() {
let err = SdkError::Registration("instance already exists".to_string());
assert_eq!(
format!("{}", err),
"registration failed: instance already exists"
);
}
#[test]
fn test_checkpoint_error_display() {
let err = SdkError::Checkpoint("failed to save state".to_string());
assert_eq!(format!("{}", err), "checkpoint error: failed to save state");
}
#[test]
fn test_sleep_error_display() {
let err = SdkError::Sleep("invalid duration".to_string());
assert_eq!(format!("{}", err), "sleep error: invalid duration");
}
#[test]
fn test_event_error_display() {
let err = SdkError::Event("failed to send event".to_string());
assert_eq!(format!("{}", err), "event error: failed to send event");
}
#[test]
fn test_signal_error_display() {
let err = SdkError::Signal("signal rejected".to_string());
assert_eq!(format!("{}", err), "signal error: signal rejected");
}
#[test]
fn test_status_error_display() {
let err = SdkError::Status("not found".to_string());
assert_eq!(format!("{}", err), "status error: not found");
}
#[test]
fn test_server_error_display() {
let err = SdkError::Server {
code: "ERR_NOT_FOUND".to_string(),
message: "Instance not found".to_string(),
};
assert_eq!(
format!("{}", err),
"server error: ERR_NOT_FOUND - Instance not found"
);
}
#[test]
fn test_cancelled_error_display() {
let err = SdkError::Cancelled;
assert_eq!(format!("{}", err), "instance cancelled");
}
#[test]
fn test_paused_error_display() {
let err = SdkError::Paused;
assert_eq!(format!("{}", err), "instance paused");
}
#[test]
fn test_serialization_error_display() {
let err = SdkError::Serialization("invalid JSON".to_string());
assert_eq!(format!("{}", err), "serialization error: invalid JSON");
}
#[test]
fn test_unexpected_response_error_display() {
let err = SdkError::UnexpectedResponse("expected Ack".to_string());
assert_eq!(format!("{}", err), "unexpected response: expected Ack");
}
#[test]
fn test_internal_error_display() {
let err = SdkError::Internal("mutex poisoned".to_string());
assert_eq!(format!("{}", err), "internal error: mutex poisoned");
}
#[test]
fn test_error_debug() {
let err = SdkError::Config("test".to_string());
let debug_str = format!("{:?}", err);
assert!(debug_str.contains("Config"));
assert!(debug_str.contains("test"));
}
#[test]
fn test_server_error_debug() {
let err = SdkError::Server {
code: "ERR_500".to_string(),
message: "Internal error".to_string(),
};
let debug_str = format!("{:?}", err);
assert!(debug_str.contains("Server"));
assert!(debug_str.contains("ERR_500"));
assert!(debug_str.contains("Internal error"));
}
#[test]
fn test_result_type_alias() {
fn returns_ok() -> Result<i32> {
Ok(42)
}
fn returns_err() -> Result<i32> {
Err(SdkError::Internal("test".to_string()))
}
assert!(returns_ok().is_ok());
assert!(returns_err().is_err());
}
}