tlq_client/
message.rs

1use serde::{Deserialize, Serialize};
2use uuid::Uuid;
3
4/// Represents a message in the TLQ queue system.
5///
6/// Each message has a unique identifier, content, and metadata about its processing state.
7/// Messages are automatically assigned UUID v7 identifiers which provide time-ordering.
8///
9/// # Examples
10///
11/// ```
12/// use tlq_client::Message;
13///
14/// // Create a new message
15/// let message = Message::new("Hello, World!".to_string());
16/// println!("Message ID: {}", message.id);
17/// println!("Message body: {}", message.body);
18/// ```
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
20pub struct Message {
21    /// Unique identifier for the message (UUID v7 format for time-ordering)
22    pub id: Uuid,
23    /// The message content/body as a string
24    pub body: String,
25    /// Current processing state of the message
26    pub state: MessageState,
27    /// Optional ISO datetime string indicating when the message lock expires
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub lock_until: Option<String>, // ISO datetime string
30    /// Number of times this message has been retried after failure
31    pub retry_count: u32,
32}
33
34/// Represents the current processing state of a message in the queue.
35///
36/// Messages transition through these states as they are processed:
37/// - `Ready` → `Processing` (when retrieved by a consumer)
38/// - `Processing` → `Failed` (if processing fails)
39/// - `Failed` → `Ready` (when retried)
40/// - Any state → deleted (when explicitly deleted)
41///
42/// # Serialization
43///
44/// States are serialized in PascalCase format ("Ready", "Processing", "Failed")
45/// to match the TLQ server API expectations.
46///
47/// # Examples
48///
49/// ```
50/// use tlq_client::MessageState;
51///
52/// let state = MessageState::Ready;
53/// assert_eq!(serde_json::to_string(&state).unwrap(), "\"Ready\"");
54/// ```
55#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
56#[serde(rename_all = "PascalCase")]
57pub enum MessageState {
58    /// Message is ready to be processed by a consumer
59    Ready,
60    /// Message is currently being processed by a consumer
61    Processing,
62    /// Message processing failed and may need to be retried
63    Failed,
64}
65
66impl Message {
67    /// Creates a new message with the specified body content.
68    ///
69    /// The message is initialized with:
70    /// - A new UUID v7 identifier (provides time-ordering)
71    /// - State set to [`MessageState::Ready`]
72    /// - No lock expiration time
73    /// - Zero retry count
74    ///
75    /// # Arguments
76    ///
77    /// * `body` - The message content as a String
78    ///
79    /// # Examples
80    ///
81    /// ```
82    /// use tlq_client::{Message, MessageState};
83    ///
84    /// let message = Message::new("Process this task".to_string());
85    /// assert_eq!(message.body, "Process this task");
86    /// assert_eq!(message.state, MessageState::Ready);
87    /// assert_eq!(message.retry_count, 0);
88    /// assert!(message.lock_until.is_none());
89    /// ```
90    pub fn new(body: String) -> Self {
91        Self {
92            id: Uuid::now_v7(),
93            body,
94            state: MessageState::Ready,
95            lock_until: None,
96            retry_count: 0,
97        }
98    }
99}
100
101// Internal request structures for TLQ API communication
102
103/// Request structure for adding a message to the queue
104#[derive(Debug, Serialize)]
105pub struct AddMessageRequest {
106    pub body: String,
107}
108
109/// Request structure for retrieving messages from the queue
110#[derive(Debug, Serialize)]
111pub struct GetMessagesRequest {
112    pub count: u32,
113}
114
115/// Request structure for deleting messages from the queue
116#[derive(Debug, Serialize)]
117pub struct DeleteMessagesRequest {
118    pub ids: Vec<Uuid>,
119}
120
121/// Request structure for retrying failed messages
122#[derive(Debug, Serialize)]
123pub struct RetryMessagesRequest {
124    pub ids: Vec<Uuid>,
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use serde_json;
131
132    #[test]
133    fn test_message_creation() {
134        let message = Message::new("Test message".to_string());
135
136        assert_eq!(message.body, "Test message");
137        assert_eq!(message.state, MessageState::Ready);
138        assert_eq!(message.retry_count, 0);
139
140        // UUID should be valid
141        assert!(!message.id.to_string().is_empty());
142    }
143
144    #[test]
145    fn test_message_state_serialization() {
146        // Test that MessageState serializes to the expected Pascal case
147        assert_eq!(
148            serde_json::to_string(&MessageState::Ready).unwrap(),
149            "\"Ready\""
150        );
151        assert_eq!(
152            serde_json::to_string(&MessageState::Processing).unwrap(),
153            "\"Processing\""
154        );
155        assert_eq!(
156            serde_json::to_string(&MessageState::Failed).unwrap(),
157            "\"Failed\""
158        );
159    }
160
161    #[test]
162    fn test_message_state_deserialization() {
163        // Test that MessageState deserializes from Pascal case
164        assert_eq!(
165            serde_json::from_str::<MessageState>("\"Ready\"").unwrap(),
166            MessageState::Ready
167        );
168        assert_eq!(
169            serde_json::from_str::<MessageState>("\"Processing\"").unwrap(),
170            MessageState::Processing
171        );
172        assert_eq!(
173            serde_json::from_str::<MessageState>("\"Failed\"").unwrap(),
174            MessageState::Failed
175        );
176    }
177
178    #[test]
179    fn test_message_state_invalid_deserialization() {
180        // Test that invalid states fail to deserialize
181        let result = serde_json::from_str::<MessageState>("\"Invalid\"");
182        assert!(result.is_err());
183
184        let result = serde_json::from_str::<MessageState>("\"ready\""); // lowercase
185        assert!(result.is_err());
186
187        let result = serde_json::from_str::<MessageState>("\"READY\""); // uppercase
188        assert!(result.is_err());
189    }
190
191    #[test]
192    fn test_message_serialization() {
193        let message = Message::new("test body".to_string());
194
195        let json = serde_json::to_string(&message).unwrap();
196
197        // Should contain all fields
198        assert!(json.contains("\"id\":"));
199        assert!(json.contains("\"body\":\"test body\""));
200        assert!(json.contains("\"state\":\"Ready\""));
201        assert!(json.contains("\"retry_count\":0"));
202
203        // Should deserialize back correctly
204        let deserialized: Message = serde_json::from_str(&json).unwrap();
205        assert_eq!(deserialized.body, message.body);
206        assert_eq!(deserialized.state, message.state);
207        assert_eq!(deserialized.retry_count, message.retry_count);
208        assert_eq!(deserialized.id, message.id);
209    }
210
211    #[test]
212    fn test_message_with_special_characters() {
213        let special_body = "Test with 🦀 emojis and \"quotes\" and \n newlines \t tabs";
214        let message = Message::new(special_body.to_string());
215
216        assert_eq!(message.body, special_body);
217
218        // Should serialize and deserialize correctly
219        let json = serde_json::to_string(&message).unwrap();
220        let deserialized: Message = serde_json::from_str(&json).unwrap();
221        assert_eq!(deserialized.body, special_body);
222    }
223
224    #[test]
225    fn test_message_with_very_long_body() {
226        let long_body = "a".repeat(100_000);
227        let message = Message::new(long_body.clone());
228
229        assert_eq!(message.body, long_body);
230        assert_eq!(message.body.len(), 100_000);
231    }
232
233    #[test]
234    fn test_message_with_empty_body() {
235        let message = Message::new("".to_string());
236
237        assert_eq!(message.body, "");
238        assert_eq!(message.state, MessageState::Ready);
239        assert_eq!(message.retry_count, 0);
240    }
241
242    #[test]
243    fn test_request_response_structures() {
244        // Test AddMessageRequest
245        let add_req = AddMessageRequest {
246            body: "test message".to_string(),
247        };
248        let json = serde_json::to_string(&add_req).unwrap();
249        assert!(json.contains("\"body\":\"test message\""));
250
251        // Test GetMessagesRequest
252        let get_req = GetMessagesRequest { count: 5 };
253        let json = serde_json::to_string(&get_req).unwrap();
254        assert!(json.contains("\"count\":5"));
255
256        // Test DeleteMessagesRequest
257        use uuid::Uuid;
258        let id1 = Uuid::now_v7();
259        let id2 = Uuid::now_v7();
260        let delete_req = DeleteMessagesRequest {
261            ids: vec![id1, id2],
262        };
263        let json = serde_json::to_string(&delete_req).unwrap();
264        assert!(json.contains("\"ids\":"));
265
266        // Test RetryMessagesRequest
267        let retry_req = RetryMessagesRequest { ids: vec![id1] };
268        let json = serde_json::to_string(&retry_req).unwrap();
269        assert!(json.contains("\"ids\":"));
270    }
271
272    #[test]
273    fn test_response_deserialization() {
274        // Test direct Message response (for add_message)
275        let message_json = r#"{"id":"0198fbd8-344e-7b70-841f-3fbd4b371e4c","body":"test","state":"Ready","lock_until":null,"retry_count":0}"#;
276        let message: Message = serde_json::from_str(message_json).unwrap();
277        assert_eq!(message.body, "test");
278        assert_eq!(message.state, MessageState::Ready);
279        assert_eq!(message.retry_count, 0);
280        assert_eq!(message.lock_until, None);
281
282        // Test array of messages response (for get_messages)
283        let messages_json = r#"[{"id":"0198fbd8-344e-7b70-841f-3fbd4b371e4c","body":"test1","state":"Processing","lock_until":null,"retry_count":1}]"#;
284        let messages: Vec<Message> = serde_json::from_str(messages_json).unwrap();
285        assert_eq!(messages.len(), 1);
286        assert_eq!(messages[0].body, "test1");
287        assert_eq!(messages[0].state, MessageState::Processing);
288
289        // Test success string responses (for delete/retry/purge)
290        let success_response: String = serde_json::from_str(r#""Success""#).unwrap();
291        assert_eq!(success_response, "Success");
292
293        // Test health check response
294        let health_response: String = serde_json::from_str(r#""Hello World""#).unwrap();
295        assert_eq!(health_response, "Hello World");
296    }
297
298    #[test]
299    fn test_malformed_response_deserialization() {
300        // Test that malformed JSON fails gracefully
301        let malformed_json = r#"{"id": invalid}"#;
302        let result = serde_json::from_str::<Message>(malformed_json);
303        assert!(result.is_err());
304
305        // Test missing required fields in Message
306        let incomplete_json = r#"{"id":"0198fbd8-344e-7b70-841f-3fbd4b371e4c","body":"test"}"#; // Missing state and retry_count
307        let result = serde_json::from_str::<Message>(incomplete_json);
308        assert!(result.is_err());
309
310        // Test wrong field types in Message
311        let wrong_type_json = r#"{"id":"0198fbd8-344e-7b70-841f-3fbd4b371e4c","body":"test","state":"Ready","retry_count":"not_a_number"}"#;
312        let result = serde_json::from_str::<Message>(wrong_type_json);
313        assert!(result.is_err());
314
315        // Test malformed message with invalid UUID
316        let bad_uuid_json = r#"{"id":"invalid-uuid","body":"test","state":"Ready","lock_until":null,"retry_count":0}"#;
317        let result = serde_json::from_str::<Message>(bad_uuid_json);
318        assert!(result.is_err()); // Should fail due to invalid UUID
319
320        // Test malformed array
321        let bad_array_json = r#"[{"id":"invalid"}]"#;
322        let result = serde_json::from_str::<Vec<Message>>(bad_array_json);
323        assert!(result.is_err());
324    }
325}