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