1use std::collections::HashMap;
4use std::fmt;
5use std::sync::Arc;
6
7use chrono::{DateTime, Utc};
8use serde_json::Value;
9
10use crate::types::message::MessageResponse;
11
12const DEBUG_CONTENT_MAX_CHARS: usize = 50;
15
16pub(crate) struct MessageInner {
17 workspace_id: String,
18 id: String,
19 content: String,
20 peer_id: String,
21 session_id: String,
22 metadata: HashMap<String, Value>,
23 created_at: DateTime<Utc>,
24 token_count: u64,
25}
26
27#[derive(Clone)]
32pub struct Message {
33 inner: Arc<MessageInner>,
34}
35
36impl Message {
37 pub(crate) fn from_raw(resp: MessageResponse) -> Self {
42 Self {
43 inner: Arc::new(MessageInner {
44 workspace_id: resp.workspace_id,
45 id: resp.id,
46 content: resp.content,
47 peer_id: resp.peer_id,
48 session_id: resp.session_id,
49 metadata: resp.metadata,
50 created_at: resp.created_at,
51 token_count: resp.token_count,
52 }),
53 }
54 }
55
56 #[must_use]
58 pub fn id(&self) -> &str {
59 &self.inner.id
60 }
61
62 #[must_use]
64 pub fn content(&self) -> &str {
65 &self.inner.content
66 }
67
68 #[must_use]
70 pub fn peer_id(&self) -> &str {
71 &self.inner.peer_id
72 }
73
74 #[must_use]
76 pub fn session_id(&self) -> &str {
77 &self.inner.session_id
78 }
79
80 #[must_use]
82 pub fn metadata(&self) -> &HashMap<String, Value> {
83 &self.inner.metadata
84 }
85
86 #[must_use]
88 pub fn created_at(&self) -> DateTime<Utc> {
89 self.inner.created_at
90 }
91
92 #[must_use]
94 pub fn token_count(&self) -> u64 {
95 self.inner.token_count
96 }
97
98 #[must_use]
100 pub fn workspace_id(&self) -> &str {
101 &self.inner.workspace_id
102 }
103}
104
105struct Truncated<'a>(&'a str);
110
111impl fmt::Debug for Truncated<'_> {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 match self.0.char_indices().nth(DEBUG_CONTENT_MAX_CHARS) {
114 Some((idx, _)) => write!(f, "\"{}...\"", self.0[..idx].escape_debug()),
118 None => fmt::Debug::fmt(self.0, f),
119 }
120 }
121}
122
123impl fmt::Debug for Message {
124 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125 f.debug_struct("Message")
126 .field("id", &self.inner.id)
127 .field("content", &Truncated(&self.inner.content))
128 .field("peer_id", &self.inner.peer_id)
129 .field("session_id", &self.inner.session_id)
130 .finish_non_exhaustive()
131 }
132}
133
134impl fmt::Display for Message {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 f.write_str(&self.inner.content)
137 }
138}
139
140#[cfg(test)]
141#[allow(
142 clippy::unwrap_used,
143 clippy::expect_used,
144 clippy::panic,
145 clippy::unnecessary_wraps,
146 clippy::needless_pass_by_value,
147 clippy::unused_async
148)]
149mod tests {
150 use static_assertions::assert_impl_all;
151
152 use super::*;
153
154 assert_impl_all!(Message: Send, Sync, Clone, fmt::Debug, fmt::Display);
155
156 fn fake_response() -> MessageResponse {
157 MessageResponse {
158 id: "msg_1".to_owned(),
159 content: "hello world".to_owned(),
160 peer_id: "peer_a".to_owned(),
161 session_id: "sess_x".to_owned(),
162 metadata: HashMap::new(),
163 created_at: Utc::now(),
164 workspace_id: "ws_1".to_owned(),
165 token_count: 3,
166 }
167 }
168
169 #[test]
170 fn from_raw_maps_fields() {
171 let resp = fake_response();
172 let msg = Message::from_raw(resp);
173 assert_eq!(msg.id(), "msg_1");
174 assert_eq!(msg.content(), "hello world");
175 assert_eq!(msg.peer_id(), "peer_a");
176 assert_eq!(msg.session_id(), "sess_x");
177 assert_eq!(msg.workspace_id(), "ws_1");
178 assert_eq!(msg.token_count(), 3);
179 assert!(msg.metadata().is_empty());
180 }
181
182 #[test]
183 fn from_raw_stores_response_workspace_id() {
184 let mut resp = fake_response();
186 resp.workspace_id = "ws_from_server".to_owned();
187 let msg = Message::from_raw(resp);
188 assert_eq!(msg.workspace_id(), "ws_from_server");
189 }
190
191 #[test]
192 fn debug_truncates_long_content() {
193 let mut resp = fake_response();
194 resp.content = "a".repeat(80);
195 let msg = Message::from_raw(resp);
196 let dbg = format!("{msg:?}");
197 assert!(dbg.contains("aaa..."));
198 assert!(!dbg.contains(&"a".repeat(80)));
199 }
200
201 #[test]
202 fn debug_short_content_not_truncated() {
203 let resp = fake_response();
204 let msg = Message::from_raw(resp);
205 let dbg = format!("{msg:?}");
206 assert!(dbg.contains("hello world"));
207 assert!(!dbg.contains("..."));
208 }
209
210 #[test]
211 fn display_returns_full_content() {
212 let mut resp = fake_response();
213 resp.content = "a".repeat(80);
214 let msg = Message::from_raw(resp);
215 assert_eq!(format!("{msg}"), "a".repeat(80));
216 }
217
218 #[test]
219 fn debug_truncation_multibyte_utf8() {
220 let mut resp = fake_response();
221 resp.content = "\u{4e00}".repeat(60);
222 let msg = Message::from_raw(resp);
223 let dbg = format!("{msg:?}");
224 assert!(dbg.contains("..."));
225 }
226}