Skip to main content

rustant_core/canvas/
components.rs

1//! Canvas component specifications.
2//!
3//! Defines structured specs for charts, tables, forms, and diagrams
4//! that the renderer converts to HTML/JS for display.
5
6use serde::{Deserialize, Serialize};
7
8/// Chart specification (rendered via Chart.js).
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct ChartSpec {
11    /// Chart type: "line", "bar", "pie", "scatter", "doughnut".
12    pub chart_type: String,
13    /// Data labels (x-axis or category labels).
14    pub labels: Vec<String>,
15    /// Dataset(s). Each dataset has a label and numeric data.
16    pub datasets: Vec<ChartDataset>,
17    /// Optional title for the chart.
18    #[serde(default)]
19    pub title: Option<String>,
20}
21
22/// A single dataset in a chart.
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct ChartDataset {
25    pub label: String,
26    pub data: Vec<f64>,
27    #[serde(default)]
28    pub color: Option<String>,
29}
30
31impl ChartSpec {
32    /// Create a simple single-dataset chart.
33    pub fn simple(chart_type: &str, labels: Vec<String>, data: Vec<f64>) -> Self {
34        Self {
35            chart_type: chart_type.into(),
36            labels,
37            datasets: vec![ChartDataset {
38                label: "Data".into(),
39                data,
40                color: None,
41            }],
42            title: None,
43        }
44    }
45
46    /// Validate that chart_type is a known type.
47    pub fn is_valid_type(&self) -> bool {
48        matches!(
49            self.chart_type.as_str(),
50            "line" | "bar" | "pie" | "scatter" | "doughnut" | "radar" | "polarArea"
51        )
52    }
53}
54
55/// Table specification (sortable HTML table).
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct TableSpec {
58    /// Column headers.
59    pub headers: Vec<String>,
60    /// Row data (each row is a vec of cell values).
61    pub rows: Vec<Vec<String>>,
62    /// Whether columns should be sortable.
63    #[serde(default)]
64    pub sortable: bool,
65}
66
67impl TableSpec {
68    pub fn new(headers: Vec<String>, rows: Vec<Vec<String>>) -> Self {
69        Self {
70            headers,
71            rows,
72            sortable: false,
73        }
74    }
75}
76
77/// Form specification (validated HTML form).
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct FormSpec {
80    /// Form fields.
81    pub fields: Vec<FormField>,
82    /// Optional submit button text.
83    #[serde(default = "default_submit_text")]
84    pub submit_text: String,
85    /// Optional form title.
86    #[serde(default)]
87    pub title: Option<String>,
88}
89
90fn default_submit_text() -> String {
91    "Submit".into()
92}
93
94/// A form field definition.
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct FormField {
97    /// Field name (used as form key).
98    pub name: String,
99    /// Display label.
100    pub label: String,
101    /// Field type: "text", "number", "email", "select", "textarea", "checkbox".
102    pub field_type: String,
103    /// Whether the field is required.
104    #[serde(default)]
105    pub required: bool,
106    /// Placeholder text.
107    #[serde(default)]
108    pub placeholder: Option<String>,
109    /// Options for select fields.
110    #[serde(default)]
111    pub options: Vec<String>,
112    /// Default value.
113    #[serde(default)]
114    pub default_value: Option<String>,
115}
116
117/// Diagram specification (rendered via Mermaid).
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct DiagramSpec {
120    /// Mermaid source code for the diagram.
121    pub source: String,
122    /// Optional title.
123    #[serde(default)]
124    pub title: Option<String>,
125}
126
127impl DiagramSpec {
128    pub fn new(source: &str) -> Self {
129        Self {
130            source: source.into(),
131            title: None,
132        }
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_chart_spec_simple() {
142        let chart = ChartSpec::simple(
143            "bar",
144            vec!["A".into(), "B".into(), "C".into()],
145            vec![1.0, 2.0, 3.0],
146        );
147        assert_eq!(chart.chart_type, "bar");
148        assert_eq!(chart.labels.len(), 3);
149        assert_eq!(chart.datasets.len(), 1);
150        assert_eq!(chart.datasets[0].data, vec![1.0, 2.0, 3.0]);
151        assert!(chart.is_valid_type());
152    }
153
154    #[test]
155    fn test_chart_spec_serialization() {
156        let chart = ChartSpec::simple("line", vec!["Jan".into(), "Feb".into()], vec![10.0, 20.0]);
157        let json = serde_json::to_string(&chart).unwrap();
158        let restored: ChartSpec = serde_json::from_str(&json).unwrap();
159        assert_eq!(restored.chart_type, "line");
160        assert_eq!(restored.datasets[0].data.len(), 2);
161    }
162
163    #[test]
164    fn test_chart_valid_types() {
165        for t in &[
166            "line",
167            "bar",
168            "pie",
169            "scatter",
170            "doughnut",
171            "radar",
172            "polarArea",
173        ] {
174            let c = ChartSpec::simple(t, vec![], vec![]);
175            assert!(c.is_valid_type(), "Expected {} to be valid", t);
176        }
177        let invalid = ChartSpec::simple("unknown", vec![], vec![]);
178        assert!(!invalid.is_valid_type());
179    }
180
181    #[test]
182    fn test_table_spec() {
183        let table = TableSpec::new(
184            vec!["Name".into(), "Age".into()],
185            vec![
186                vec!["Alice".into(), "30".into()],
187                vec!["Bob".into(), "25".into()],
188            ],
189        );
190        assert_eq!(table.headers.len(), 2);
191        assert_eq!(table.rows.len(), 2);
192        assert!(!table.sortable);
193    }
194
195    #[test]
196    fn test_table_spec_serialization() {
197        let table = TableSpec {
198            headers: vec!["Col1".into()],
199            rows: vec![vec!["Val1".into()]],
200            sortable: true,
201        };
202        let json = serde_json::to_string(&table).unwrap();
203        let restored: TableSpec = serde_json::from_str(&json).unwrap();
204        assert!(restored.sortable);
205    }
206
207    #[test]
208    fn test_form_spec() {
209        let form = FormSpec {
210            fields: vec![FormField {
211                name: "email".into(),
212                label: "Email".into(),
213                field_type: "email".into(),
214                required: true,
215                placeholder: Some("user@example.com".into()),
216                options: vec![],
217                default_value: None,
218            }],
219            submit_text: "Send".into(),
220            title: Some("Contact".into()),
221        };
222        assert_eq!(form.fields.len(), 1);
223        assert!(form.fields[0].required);
224    }
225
226    #[test]
227    fn test_form_spec_serialization() {
228        let form = FormSpec {
229            fields: vec![FormField {
230                name: "name".into(),
231                label: "Name".into(),
232                field_type: "text".into(),
233                required: false,
234                placeholder: None,
235                options: vec![],
236                default_value: Some("default".into()),
237            }],
238            submit_text: "Submit".into(),
239            title: None,
240        };
241        let json = serde_json::to_string(&form).unwrap();
242        let restored: FormSpec = serde_json::from_str(&json).unwrap();
243        assert_eq!(restored.fields[0].default_value, Some("default".into()));
244    }
245
246    #[test]
247    fn test_diagram_spec() {
248        let diagram = DiagramSpec::new("graph LR; A-->B; B-->C");
249        assert!(diagram.source.contains("graph LR"));
250        assert!(diagram.title.is_none());
251    }
252
253    #[test]
254    fn test_diagram_spec_serialization() {
255        let diagram = DiagramSpec {
256            source: "sequenceDiagram\n  A->>B: Hello".into(),
257            title: Some("Sequence".into()),
258        };
259        let json = serde_json::to_string(&diagram).unwrap();
260        let restored: DiagramSpec = serde_json::from_str(&json).unwrap();
261        assert_eq!(restored.title, Some("Sequence".into()));
262    }
263}