use std::io;
use std::path::PathBuf;
use thiserror::Error;
pub type Result<T> = std::result::Result<T, ChatpackError>;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ChatpackError {
#[error("IO error: {0}")]
Io(#[from] io::Error),
#[error("Failed to parse {format} export{}: {source}", path.as_ref().map(|p| format!(" (file: {})", p.display())).unwrap_or_default())]
Parse {
format: &'static str,
#[source]
source: ParseErrorKind,
path: Option<PathBuf>,
},
#[error("Invalid {format} format: {message}")]
InvalidFormat {
format: &'static str,
message: String,
},
#[error("Invalid date '{input}'. Expected format: {expected}")]
InvalidDate {
input: String,
expected: &'static str,
},
#[cfg(any(feature = "csv-output", feature = "discord"))]
#[error("CSV error: {0}")]
Csv(#[from] csv::Error),
#[cfg(any(
feature = "telegram",
feature = "instagram",
feature = "discord",
feature = "json-output"
))]
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("Streaming error: {0}")]
Streaming(#[source] StreamingErrorKind),
#[error("UTF-8 encoding error in {context}: {source}")]
Utf8 {
context: String,
#[source]
source: std::string::FromUtf8Error,
},
#[error("Message too large: {actual_size} bytes (maximum: {max_size} bytes)")]
BufferOverflow {
max_size: usize,
actual_size: usize,
},
#[error("Unexpected end of file while {context}")]
UnexpectedEof {
context: String,
},
}
#[derive(Debug, Error)]
pub enum ParseErrorKind {
#[cfg(any(
feature = "telegram",
feature = "instagram",
feature = "discord",
feature = "json-output"
))]
#[error("{0}")]
Json(#[from] serde_json::Error),
#[error("{0}")]
Pattern(String),
#[error("{0}")]
Other(String),
}
#[derive(Debug, Error)]
pub enum StreamingErrorKind {
#[error("IO error: {0}")]
Io(#[from] io::Error),
#[cfg(any(
feature = "telegram",
feature = "instagram",
feature = "discord",
feature = "json-output"
))]
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("Invalid format: {0}")]
InvalidFormat(String),
#[error("Buffer overflow: {actual_size} bytes (max: {max_size})")]
BufferOverflow { max_size: usize, actual_size: usize },
#[error("Unexpected end of file")]
UnexpectedEof,
}
impl From<std::string::FromUtf8Error> for ChatpackError {
fn from(err: std::string::FromUtf8Error) -> Self {
ChatpackError::Utf8 {
context: "output conversion".to_string(),
source: err,
}
}
}
impl ChatpackError {
#[cfg(feature = "telegram")]
pub fn telegram_parse(source: serde_json::Error, path: Option<PathBuf>) -> Self {
ChatpackError::Parse {
format: "Telegram JSON",
source: ParseErrorKind::Json(source),
path,
}
}
pub fn whatsapp_parse(message: impl Into<String>, path: Option<PathBuf>) -> Self {
ChatpackError::Parse {
format: "WhatsApp TXT",
source: ParseErrorKind::Pattern(message.into()),
path,
}
}
#[cfg(feature = "instagram")]
pub fn instagram_parse(source: serde_json::Error, path: Option<PathBuf>) -> Self {
ChatpackError::Parse {
format: "Instagram JSON",
source: ParseErrorKind::Json(source),
path,
}
}
#[cfg(feature = "discord")]
pub fn discord_parse(source: serde_json::Error, path: Option<PathBuf>) -> Self {
ChatpackError::Parse {
format: "Discord",
source: ParseErrorKind::Json(source),
path,
}
}
pub fn invalid_format(format: &'static str, message: impl Into<String>) -> Self {
ChatpackError::InvalidFormat {
format,
message: message.into(),
}
}
pub fn invalid_date(input: impl Into<String>) -> Self {
ChatpackError::InvalidDate {
input: input.into(),
expected: "YYYY-MM-DD",
}
}
pub fn streaming(kind: StreamingErrorKind) -> Self {
ChatpackError::Streaming(kind)
}
pub fn buffer_overflow(max_size: usize, actual_size: usize) -> Self {
ChatpackError::BufferOverflow {
max_size,
actual_size,
}
}
pub fn unexpected_eof(context: impl Into<String>) -> Self {
ChatpackError::UnexpectedEof {
context: context.into(),
}
}
pub fn is_io(&self) -> bool {
matches!(self, ChatpackError::Io(_))
}
pub fn is_parse(&self) -> bool {
matches!(self, ChatpackError::Parse { .. })
}
pub fn is_invalid_format(&self) -> bool {
matches!(self, ChatpackError::InvalidFormat { .. })
}
pub fn is_invalid_date(&self) -> bool {
matches!(self, ChatpackError::InvalidDate { .. })
}
}
#[cfg(all(
feature = "streaming",
any(
feature = "telegram",
feature = "whatsapp",
feature = "instagram",
feature = "discord"
)
))]
impl From<crate::streaming::StreamingError> for ChatpackError {
#[allow(unreachable_patterns)]
fn from(err: crate::streaming::StreamingError) -> Self {
match err {
crate::streaming::StreamingError::Io(e) => {
ChatpackError::Streaming(StreamingErrorKind::Io(e))
}
#[cfg(any(feature = "telegram", feature = "instagram", feature = "discord"))]
crate::streaming::StreamingError::Json(e) => {
ChatpackError::Streaming(StreamingErrorKind::Json(e))
}
crate::streaming::StreamingError::InvalidFormat(s) => {
ChatpackError::Streaming(StreamingErrorKind::InvalidFormat(s))
}
crate::streaming::StreamingError::BufferOverflow {
max_size,
actual_size,
} => ChatpackError::Streaming(StreamingErrorKind::BufferOverflow {
max_size,
actual_size,
}),
crate::streaming::StreamingError::UnexpectedEof => {
ChatpackError::Streaming(StreamingErrorKind::UnexpectedEof)
}
_ => ChatpackError::Streaming(StreamingErrorKind::InvalidFormat(
"Unknown streaming error".to_string(),
)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_io_error_display() {
let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
let err = ChatpackError::from(io_err);
let display = err.to_string();
assert!(display.contains("IO error"));
assert!(display.contains("file not found"));
}
#[cfg(any(
feature = "telegram",
feature = "instagram",
feature = "discord",
feature = "json-output"
))]
#[test]
fn test_parse_error_with_path() {
let json_err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
let err = ChatpackError::Parse {
format: "Telegram JSON",
source: ParseErrorKind::Json(json_err),
path: Some(PathBuf::from("/path/to/file.json")),
};
let display = err.to_string();
assert!(display.contains("Telegram JSON"));
assert!(display.contains("/path/to/file.json"));
}
#[test]
fn test_parse_error_without_path() {
let err = ChatpackError::Parse {
format: "WhatsApp TXT",
source: ParseErrorKind::Pattern("invalid pattern".into()),
path: None,
};
let display = err.to_string();
assert!(display.contains("WhatsApp TXT"));
assert!(!display.contains("file:"));
}
#[test]
fn test_parse_error_other_kind() {
let err = ChatpackError::Parse {
format: "Test",
source: ParseErrorKind::Other("custom error".into()),
path: None,
};
let display = err.to_string();
assert!(display.contains("custom error"));
}
#[test]
fn test_invalid_format_display() {
let err = ChatpackError::InvalidFormat {
format: "Discord",
message: "unrecognized export format".into(),
};
let display = err.to_string();
assert!(display.contains("Discord"));
assert!(display.contains("unrecognized export format"));
}
#[test]
fn test_invalid_date_display() {
let err = ChatpackError::invalid_date("not-a-date");
let display = err.to_string();
assert!(display.contains("not-a-date"));
assert!(display.contains("YYYY-MM-DD"));
}
#[test]
fn test_buffer_overflow_display() {
let err = ChatpackError::buffer_overflow(1024, 2048);
let display = err.to_string();
assert!(display.contains("2048"));
assert!(display.contains("1024"));
}
#[test]
fn test_unexpected_eof_display() {
let err = ChatpackError::unexpected_eof("parsing JSON array");
let display = err.to_string();
assert!(display.contains("Unexpected end of file"));
assert!(display.contains("parsing JSON array"));
}
#[test]
fn test_streaming_error_display() {
let err =
ChatpackError::Streaming(StreamingErrorKind::InvalidFormat("missing header".into()));
let display = err.to_string();
assert!(display.contains("Streaming error"));
assert!(display.contains("missing header"));
}
#[test]
fn test_utf8_error_display() {
let invalid_bytes = vec![0xff, 0xfe];
let utf8_err = String::from_utf8(invalid_bytes).unwrap_err();
let err = ChatpackError::Utf8 {
context: "reading file".into(),
source: utf8_err,
};
let display = err.to_string();
assert!(display.contains("UTF-8"));
assert!(display.contains("reading file"));
}
#[test]
fn test_error_source_chain() {
use std::error::Error;
let io_err = io::Error::new(io::ErrorKind::PermissionDenied, "access denied");
let err = ChatpackError::from(io_err);
assert!(err.source().is_some());
}
#[test]
fn test_streaming_error_source() {
use std::error::Error;
let io_err = io::Error::new(io::ErrorKind::NotFound, "not found");
let streaming_err = StreamingErrorKind::Io(io_err);
let err = ChatpackError::Streaming(streaming_err);
assert!(err.source().is_some());
}
#[test]
fn test_is_methods() {
let io_err = ChatpackError::Io(io::Error::new(io::ErrorKind::NotFound, ""));
assert!(io_err.is_io());
assert!(!io_err.is_parse());
assert!(!io_err.is_invalid_format());
assert!(!io_err.is_invalid_date());
let date_err = ChatpackError::invalid_date("bad");
assert!(date_err.is_invalid_date());
assert!(!date_err.is_io());
assert!(!date_err.is_parse());
assert!(!date_err.is_invalid_format());
}
#[test]
fn test_is_parse() {
let err = ChatpackError::Parse {
format: "Test",
source: ParseErrorKind::Other("test".into()),
path: None,
};
assert!(err.is_parse());
assert!(!err.is_io());
}
#[test]
fn test_is_invalid_format() {
let err = ChatpackError::invalid_format("Test", "bad format");
assert!(err.is_invalid_format());
assert!(!err.is_parse());
}
#[test]
fn test_convenience_constructors() {
let err = ChatpackError::invalid_format("WhatsApp", "could not detect date format");
assert!(err.is_invalid_format());
assert!(err.to_string().contains("WhatsApp"));
let err = ChatpackError::unexpected_eof("reading message array");
let display = err.to_string();
assert!(display.contains("reading message array"));
}
#[test]
fn test_whatsapp_parse_constructor() {
let err = ChatpackError::whatsapp_parse("invalid format", None);
assert!(err.is_parse());
assert!(err.to_string().contains("WhatsApp TXT"));
let err_with_path =
ChatpackError::whatsapp_parse("invalid format", Some(PathBuf::from("/test.txt")));
assert!(err_with_path.to_string().contains("/test.txt"));
}
#[cfg(feature = "telegram")]
#[test]
fn test_telegram_parse_constructor() {
let json_err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
let err = ChatpackError::telegram_parse(json_err, None);
assert!(err.is_parse());
assert!(err.to_string().contains("Telegram JSON"));
}
#[cfg(feature = "instagram")]
#[test]
fn test_instagram_parse_constructor() {
let json_err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
let err = ChatpackError::instagram_parse(json_err, None);
assert!(err.is_parse());
assert!(err.to_string().contains("Instagram JSON"));
}
#[cfg(feature = "discord")]
#[test]
fn test_discord_parse_constructor() {
let json_err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
let err = ChatpackError::discord_parse(json_err, None);
assert!(err.is_parse());
assert!(err.to_string().contains("Discord"));
}
#[test]
fn test_streaming_constructor() {
let kind = StreamingErrorKind::UnexpectedEof;
let err = ChatpackError::streaming(kind);
assert!(err.to_string().contains("Streaming error"));
}
#[test]
fn test_from_io_error() {
let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
let err: ChatpackError = io_err.into();
assert!(err.is_io());
}
#[cfg(any(feature = "csv-output", feature = "discord"))]
#[test]
fn test_from_csv_error() {
use std::io::Cursor;
let mut wtr = csv::Writer::from_writer(Cursor::new(Vec::new()));
wtr.write_record(["a", "b"]).expect("write");
let data = "field1,field2\n\"unclosed";
let mut rdr = csv::ReaderBuilder::new().from_reader(data.as_bytes());
for result in rdr.records() {
if let Err(csv_err) = result {
let err: ChatpackError = csv_err.into();
assert!(err.to_string().contains("CSV error"));
return;
}
}
let io_err = std::io::Error::other("test");
let csv_err = csv::Error::from(io_err);
let err: ChatpackError = csv_err.into();
assert!(err.to_string().contains("CSV error"));
}
#[cfg(any(
feature = "telegram",
feature = "instagram",
feature = "discord",
feature = "json-output"
))]
#[test]
fn test_from_json_error() {
let json_err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
let err: ChatpackError = json_err.into();
assert!(err.to_string().contains("JSON error"));
}
#[test]
fn test_from_utf8_error() {
let invalid_bytes = vec![0xff, 0xfe];
let utf8_err = String::from_utf8(invalid_bytes).unwrap_err();
let err: ChatpackError = utf8_err.into();
assert!(err.to_string().contains("UTF-8"));
}
#[test]
fn test_streaming_error_kind_io() {
let io_err = io::Error::new(io::ErrorKind::NotFound, "not found");
let kind = StreamingErrorKind::Io(io_err);
assert!(kind.to_string().contains("IO error"));
}
#[cfg(any(
feature = "telegram",
feature = "instagram",
feature = "discord",
feature = "json-output"
))]
#[test]
fn test_streaming_error_kind_json() {
let json_err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
let kind = StreamingErrorKind::Json(json_err);
assert!(kind.to_string().contains("JSON error"));
}
#[test]
fn test_streaming_error_kind_invalid_format() {
let kind = StreamingErrorKind::InvalidFormat("missing messages array".into());
assert!(kind.to_string().contains("Invalid format"));
assert!(kind.to_string().contains("missing messages array"));
}
#[test]
fn test_streaming_error_kind_buffer_overflow() {
let kind = StreamingErrorKind::BufferOverflow {
max_size: 1024,
actual_size: 2048,
};
let display = kind.to_string();
assert!(display.contains("Buffer overflow"));
assert!(display.contains("1024"));
assert!(display.contains("2048"));
}
#[test]
fn test_streaming_error_kind_unexpected_eof() {
let kind = StreamingErrorKind::UnexpectedEof;
assert!(kind.to_string().contains("Unexpected end of file"));
}
#[test]
fn test_parse_error_kind_pattern() {
let kind = ParseErrorKind::Pattern("invalid regex".into());
assert!(kind.to_string().contains("invalid regex"));
}
#[test]
fn test_parse_error_kind_other() {
let kind = ParseErrorKind::Other("unknown error".into());
assert!(kind.to_string().contains("unknown error"));
}
#[cfg(any(
feature = "telegram",
feature = "instagram",
feature = "discord",
feature = "json-output"
))]
#[test]
fn test_parse_error_kind_json() {
let json_err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
let kind = ParseErrorKind::Json(json_err);
assert!(!kind.to_string().is_empty());
}
#[test]
fn test_result_type_alias() {
fn returns_result() -> i32 {
42
}
fn returns_error() -> Result<i32> {
Err(ChatpackError::invalid_date("bad"))
}
fn returns_ok() -> i32 {
42
}
assert_eq!(returns_result(), 42);
assert!(returns_error().is_err());
assert_eq!(returns_ok(), 42);
assert_eq!(returns_ok(), 42);
}
#[test]
fn test_error_debug() {
let err = ChatpackError::invalid_date("bad");
let debug = format!("{:?}", err);
assert!(debug.contains("InvalidDate"));
}
}