1use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8#[serde(tag = "type")]
9pub enum CanvasMessage {
10 Push {
12 target: CanvasTarget,
13 content_type: ContentType,
14 content: String,
15 },
16 Clear { target: CanvasTarget },
18 Update {
20 target: CanvasTarget,
21 content_type: ContentType,
22 content: String,
23 },
24 Interact {
26 action: String,
27 selector: String,
28 data: serde_json::Value,
29 },
30 Event {
32 event_type: String,
33 target: CanvasTarget,
34 data: serde_json::Value,
35 },
36 State { target: CanvasTarget },
38 Snapshot {
40 target: CanvasTarget,
41 items: Vec<CanvasItem>,
42 },
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(rename_all = "lowercase")]
48pub enum ContentType {
49 Html,
50 Markdown,
51 Code,
52 Chart,
53 Table,
54 Form,
55 Image,
56 Diagram,
57}
58
59impl ContentType {
60 pub fn from_str_loose(s: &str) -> Option<ContentType> {
62 match s.to_lowercase().as_str() {
63 "html" => Some(ContentType::Html),
64 "markdown" | "md" => Some(ContentType::Markdown),
65 "code" => Some(ContentType::Code),
66 "chart" => Some(ContentType::Chart),
67 "table" => Some(ContentType::Table),
68 "form" => Some(ContentType::Form),
69 "image" | "img" => Some(ContentType::Image),
70 "diagram" | "mermaid" => Some(ContentType::Diagram),
71 _ => None,
72 }
73 }
74}
75
76#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
78#[serde(untagged)]
79pub enum CanvasTarget {
80 #[default]
82 Broadcast,
83 Named(String),
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct CanvasItem {
90 pub id: Uuid,
91 pub content_type: ContentType,
92 pub content: String,
93 pub created_at: chrono::DateTime<chrono::Utc>,
94}
95
96impl CanvasItem {
97 pub fn new(content_type: ContentType, content: String) -> Self {
98 Self {
99 id: Uuid::new_v4(),
100 content_type,
101 content,
102 created_at: chrono::Utc::now(),
103 }
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn test_canvas_message_push_serialization() {
113 let msg = CanvasMessage::Push {
114 target: CanvasTarget::Broadcast,
115 content_type: ContentType::Html,
116 content: "<h1>Hello</h1>".into(),
117 };
118 let json = serde_json::to_string(&msg).unwrap();
119 assert!(json.contains("Push"));
120 let restored: CanvasMessage = serde_json::from_str(&json).unwrap();
121 match restored {
122 CanvasMessage::Push {
123 content_type,
124 content,
125 ..
126 } => {
127 assert_eq!(content_type, ContentType::Html);
128 assert_eq!(content, "<h1>Hello</h1>");
129 }
130 _ => panic!("Wrong variant"),
131 }
132 }
133
134 #[test]
135 fn test_canvas_message_clear_serialization() {
136 let msg = CanvasMessage::Clear {
137 target: CanvasTarget::Named("main".into()),
138 };
139 let json = serde_json::to_string(&msg).unwrap();
140 let restored: CanvasMessage = serde_json::from_str(&json).unwrap();
141 match restored {
142 CanvasMessage::Clear { target } => {
143 assert_eq!(target, CanvasTarget::Named("main".into()));
144 }
145 _ => panic!("Wrong variant"),
146 }
147 }
148
149 #[test]
150 fn test_canvas_message_update_serialization() {
151 let msg = CanvasMessage::Update {
152 target: CanvasTarget::Broadcast,
153 content_type: ContentType::Markdown,
154 content: "# Updated".into(),
155 };
156 let json = serde_json::to_string(&msg).unwrap();
157 let _: CanvasMessage = serde_json::from_str(&json).unwrap();
158 }
159
160 #[test]
161 fn test_canvas_message_interact_serialization() {
162 let msg = CanvasMessage::Interact {
163 action: "click".into(),
164 selector: "#submit".into(),
165 data: serde_json::json!({"value": "ok"}),
166 };
167 let json = serde_json::to_string(&msg).unwrap();
168 let _: CanvasMessage = serde_json::from_str(&json).unwrap();
169 }
170
171 #[test]
172 fn test_canvas_message_event_serialization() {
173 let msg = CanvasMessage::Event {
174 event_type: "form_submit".into(),
175 target: CanvasTarget::Broadcast,
176 data: serde_json::json!({"field": "value"}),
177 };
178 let json = serde_json::to_string(&msg).unwrap();
179 let _: CanvasMessage = serde_json::from_str(&json).unwrap();
180 }
181
182 #[test]
183 fn test_canvas_message_state_serialization() {
184 let msg = CanvasMessage::State {
185 target: CanvasTarget::Broadcast,
186 };
187 let json = serde_json::to_string(&msg).unwrap();
188 let _: CanvasMessage = serde_json::from_str(&json).unwrap();
189 }
190
191 #[test]
192 fn test_canvas_message_snapshot_serialization() {
193 let msg = CanvasMessage::Snapshot {
194 target: CanvasTarget::Broadcast,
195 items: vec![CanvasItem::new(ContentType::Html, "<p>Test</p>".into())],
196 };
197 let json = serde_json::to_string(&msg).unwrap();
198 let restored: CanvasMessage = serde_json::from_str(&json).unwrap();
199 match restored {
200 CanvasMessage::Snapshot { items, .. } => {
201 assert_eq!(items.len(), 1);
202 assert_eq!(items[0].content_type, ContentType::Html);
203 }
204 _ => panic!("Wrong variant"),
205 }
206 }
207
208 #[test]
209 fn test_content_type_parsing() {
210 assert_eq!(ContentType::from_str_loose("html"), Some(ContentType::Html));
211 assert_eq!(
212 ContentType::from_str_loose("markdown"),
213 Some(ContentType::Markdown)
214 );
215 assert_eq!(
216 ContentType::from_str_loose("md"),
217 Some(ContentType::Markdown)
218 );
219 assert_eq!(ContentType::from_str_loose("code"), Some(ContentType::Code));
220 assert_eq!(
221 ContentType::from_str_loose("chart"),
222 Some(ContentType::Chart)
223 );
224 assert_eq!(
225 ContentType::from_str_loose("table"),
226 Some(ContentType::Table)
227 );
228 assert_eq!(ContentType::from_str_loose("form"), Some(ContentType::Form));
229 assert_eq!(
230 ContentType::from_str_loose("image"),
231 Some(ContentType::Image)
232 );
233 assert_eq!(ContentType::from_str_loose("img"), Some(ContentType::Image));
234 assert_eq!(
235 ContentType::from_str_loose("diagram"),
236 Some(ContentType::Diagram)
237 );
238 assert_eq!(
239 ContentType::from_str_loose("mermaid"),
240 Some(ContentType::Diagram)
241 );
242 assert_eq!(ContentType::from_str_loose("invalid"), None);
243 }
244
245 #[test]
246 fn test_content_type_case_insensitive() {
247 assert_eq!(ContentType::from_str_loose("HTML"), Some(ContentType::Html));
248 assert_eq!(
249 ContentType::from_str_loose("Chart"),
250 Some(ContentType::Chart)
251 );
252 }
253
254 #[test]
255 fn test_canvas_target_default() {
256 let target = CanvasTarget::default();
257 assert_eq!(target, CanvasTarget::Broadcast);
258 }
259}