tuitbot_core/toolkit/
mod.rs1pub mod engage;
11pub mod media;
12pub mod read;
13pub mod write;
14
15#[cfg(test)]
16mod e2e_tests;
17
18use crate::error::XApiError;
19
20pub const MAX_TWEET_LENGTH: usize = 280;
22
23#[derive(Debug, thiserror::Error)]
28pub enum ToolkitError {
29 #[error(transparent)]
31 XApi(#[from] XApiError),
32
33 #[error("invalid input: {message}")]
35 InvalidInput { message: String },
36
37 #[error("tweet too long: {length} characters (max {max})")]
39 TweetTooLong { length: usize, max: usize },
40
41 #[error("unsupported media type for file: {path}")]
43 UnsupportedMediaType { path: String },
44
45 #[error("media too large: {size} bytes (max {max} for {media_type})")]
47 MediaTooLarge {
48 size: u64,
49 max: u64,
50 media_type: String,
51 },
52
53 #[error("thread failed at tweet {failed_index}: posted {posted}/{total} tweets")]
55 ThreadPartialFailure {
56 posted_ids: Vec<String>,
58 failed_index: usize,
60 posted: usize,
62 total: usize,
64 #[source]
66 source: Box<XApiError>,
67 },
68}
69
70pub fn validate_tweet_length(text: &str) -> Result<(), ToolkitError> {
72 if text.len() > MAX_TWEET_LENGTH {
73 return Err(ToolkitError::TweetTooLong {
74 length: text.len(),
75 max: MAX_TWEET_LENGTH,
76 });
77 }
78 Ok(())
79}
80
81fn validate_id(id: &str, name: &str) -> Result<(), ToolkitError> {
83 if id.is_empty() {
84 return Err(ToolkitError::InvalidInput {
85 message: format!("{name} must not be empty"),
86 });
87 }
88 Ok(())
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn validate_tweet_length_ok() {
97 assert!(validate_tweet_length("Hello world").is_ok());
98 }
99
100 #[test]
101 fn validate_tweet_length_exactly_280() {
102 let text = "a".repeat(280);
103 assert!(validate_tweet_length(&text).is_ok());
104 }
105
106 #[test]
107 fn validate_tweet_length_too_long() {
108 let text = "a".repeat(281);
109 let err = validate_tweet_length(&text).unwrap_err();
110 assert!(matches!(
111 err,
112 ToolkitError::TweetTooLong {
113 length: 281,
114 max: 280
115 }
116 ));
117 }
118
119 #[test]
120 fn validate_id_ok() {
121 assert!(validate_id("123", "tweet_id").is_ok());
122 }
123
124 #[test]
125 fn validate_id_empty() {
126 let err = validate_id("", "tweet_id").unwrap_err();
127 assert!(matches!(err, ToolkitError::InvalidInput { .. }));
128 }
129
130 #[test]
131 fn toolkit_error_display() {
132 let err = ToolkitError::TweetTooLong {
133 length: 300,
134 max: 280,
135 };
136 assert_eq!(err.to_string(), "tweet too long: 300 characters (max 280)");
137 }
138
139 #[test]
140 fn toolkit_error_from_x_api() {
141 let xe = XApiError::ApiError {
142 status: 404,
143 message: "Not found".to_string(),
144 };
145 let te: ToolkitError = xe.into();
146 assert!(matches!(te, ToolkitError::XApi(_)));
147 }
148}