rustant_core/canvas/
mod.rs1pub mod components;
8pub mod protocol;
9pub mod renderer;
10
11pub use components::{ChartDataset, ChartSpec, DiagramSpec, FormField, FormSpec, TableSpec};
12pub use protocol::{CanvasItem, CanvasMessage, CanvasTarget, ContentType};
13pub use renderer::{
14 render_chart_config, render_diagram_mermaid, render_form_html, render_table_html,
15};
16
17use std::collections::HashMap;
18
19const MAX_CONTENT_SIZE: usize = 1_048_576;
21
22#[derive(Debug, Default)]
24pub struct CanvasManager {
25 targets: HashMap<String, Vec<CanvasItem>>,
27 max_content_size: usize,
29}
30
31impl CanvasManager {
32 pub fn new() -> Self {
33 Self {
34 targets: HashMap::new(),
35 max_content_size: MAX_CONTENT_SIZE,
36 }
37 }
38
39 pub fn set_max_content_size(&mut self, size: usize) {
41 self.max_content_size = size;
42 }
43
44 pub fn push(
46 &mut self,
47 target: &CanvasTarget,
48 content_type: ContentType,
49 content: String,
50 ) -> Result<uuid::Uuid, CanvasError> {
51 if content.len() > self.max_content_size {
52 return Err(CanvasError::ContentTooLarge {
53 size: content.len(),
54 max: self.max_content_size,
55 });
56 }
57 let item = CanvasItem::new(content_type, content);
58 let id = item.id;
59 let key = target_key(target);
60 self.targets.entry(key).or_default().push(item);
61 Ok(id)
62 }
63
64 pub fn clear(&mut self, target: &CanvasTarget) {
66 let key = target_key(target);
67 self.targets.remove(&key);
68 }
69
70 pub fn snapshot(&self, target: &CanvasTarget) -> Vec<&CanvasItem> {
72 let key = target_key(target);
73 self.targets
74 .get(&key)
75 .map(|items| items.iter().collect())
76 .unwrap_or_default()
77 }
78
79 pub fn total_items(&self) -> usize {
81 self.targets.values().map(|v| v.len()).sum()
82 }
83
84 pub fn is_empty(&self, target: &CanvasTarget) -> bool {
86 let key = target_key(target);
87 self.targets.get(&key).map(|v| v.is_empty()).unwrap_or(true)
88 }
89}
90
91fn target_key(target: &CanvasTarget) -> String {
92 match target {
93 CanvasTarget::Broadcast => "broadcast".into(),
94 CanvasTarget::Named(name) => name.clone(),
95 }
96}
97
98#[derive(Debug, thiserror::Error)]
100pub enum CanvasError {
101 #[error("Content too large: {size} bytes exceeds maximum {max} bytes")]
102 ContentTooLarge { size: usize, max: usize },
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 #[test]
110 fn test_canvas_manager_push_and_snapshot() {
111 let mut mgr = CanvasManager::new();
112 let target = CanvasTarget::Broadcast;
113
114 let id = mgr
115 .push(&target, ContentType::Html, "<h1>Hello</h1>".into())
116 .unwrap();
117 assert!(!id.is_nil());
118
119 let items = mgr.snapshot(&target);
120 assert_eq!(items.len(), 1);
121 assert_eq!(items[0].content, "<h1>Hello</h1>");
122 assert_eq!(items[0].content_type, ContentType::Html);
123 }
124
125 #[test]
126 fn test_canvas_manager_push_multiple() {
127 let mut mgr = CanvasManager::new();
128 let target = CanvasTarget::Broadcast;
129
130 mgr.push(&target, ContentType::Html, "<p>1</p>".into())
131 .unwrap();
132 mgr.push(&target, ContentType::Markdown, "# Title".into())
133 .unwrap();
134 mgr.push(&target, ContentType::Code, "fn main() {}".into())
135 .unwrap();
136
137 assert_eq!(mgr.snapshot(&target).len(), 3);
138 assert_eq!(mgr.total_items(), 3);
139 }
140
141 #[test]
142 fn test_canvas_manager_clear() {
143 let mut mgr = CanvasManager::new();
144 let target = CanvasTarget::Broadcast;
145
146 mgr.push(&target, ContentType::Html, "<p>data</p>".into())
147 .unwrap();
148 assert!(!mgr.is_empty(&target));
149
150 mgr.clear(&target);
151 assert!(mgr.is_empty(&target));
152 assert_eq!(mgr.snapshot(&target).len(), 0);
153 }
154
155 #[test]
156 fn test_canvas_manager_named_targets() {
157 let mut mgr = CanvasManager::new();
158 let main = CanvasTarget::Named("main".into());
159 let sidebar = CanvasTarget::Named("sidebar".into());
160
161 mgr.push(&main, ContentType::Html, "<p>main</p>".into())
162 .unwrap();
163 mgr.push(&sidebar, ContentType::Html, "<p>side</p>".into())
164 .unwrap();
165
166 assert_eq!(mgr.snapshot(&main).len(), 1);
167 assert_eq!(mgr.snapshot(&sidebar).len(), 1);
168 assert_eq!(mgr.total_items(), 2);
169
170 mgr.clear(&main);
172 assert!(mgr.is_empty(&main));
173 assert!(!mgr.is_empty(&sidebar));
174 }
175
176 #[test]
177 fn test_canvas_manager_max_content_size() {
178 let mut mgr = CanvasManager::new();
179 mgr.set_max_content_size(10);
180
181 let target = CanvasTarget::Broadcast;
182 let result = mgr.push(&target, ContentType::Html, "short".into());
183 assert!(result.is_ok());
184
185 let result = mgr.push(&target, ContentType::Html, "this is way too long".into());
186 assert!(result.is_err());
187 match result {
188 Err(CanvasError::ContentTooLarge { size, max }) => {
189 assert_eq!(max, 10);
190 assert!(size > 10);
191 }
192 _ => panic!("Expected ContentTooLarge"),
193 }
194 }
195
196 #[test]
197 fn test_canvas_snapshot_captures_state() {
198 let mut mgr = CanvasManager::new();
199 let target = CanvasTarget::Broadcast;
200
201 mgr.push(&target, ContentType::Chart, "{\"type\":\"bar\"}".into())
202 .unwrap();
203 mgr.push(&target, ContentType::Table, "{\"headers\":[\"A\"]}".into())
204 .unwrap();
205
206 let snap = mgr.snapshot(&target);
207 assert_eq!(snap.len(), 2);
208 assert_eq!(snap[0].content_type, ContentType::Chart);
209 assert_eq!(snap[1].content_type, ContentType::Table);
210 }
211
212 #[test]
213 fn test_canvas_empty_target() {
214 let mgr = CanvasManager::new();
215 let target = CanvasTarget::Named("nonexistent".into());
216 assert!(mgr.is_empty(&target));
217 assert_eq!(mgr.snapshot(&target).len(), 0);
218 }
219}