mockforge_foundation/state_machine/
visual_layout.rs1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
14#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
15pub struct VisualLayout {
16 pub nodes: Vec<VisualNode>,
18
19 pub edges: Vec<VisualEdge>,
21
22 pub viewport: Option<Viewport>,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
29pub struct VisualNode {
30 pub id: String,
32
33 #[serde(rename = "type")]
35 pub node_type: String,
36
37 pub position_x: f64,
39
40 pub position_y: f64,
42
43 #[serde(default = "default_width")]
45 pub width: f64,
46
47 #[serde(default = "default_height")]
49 pub height: f64,
50
51 pub label: String,
53
54 #[serde(default)]
56 pub style: HashMap<String, serde_json::Value>,
57
58 #[serde(default)]
60 pub data: HashMap<String, serde_json::Value>,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
65#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
66pub struct VisualEdge {
67 pub id: String,
69
70 pub source: String,
72
73 pub target: String,
75
76 #[serde(default)]
78 pub label: Option<String>,
79
80 #[serde(rename = "type", default = "default_edge_type")]
82 pub edge_type: String,
83
84 #[serde(default)]
86 pub animated: bool,
87
88 #[serde(default)]
90 pub style: HashMap<String, serde_json::Value>,
91
92 #[serde(default)]
94 pub data: HashMap<String, serde_json::Value>,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
99#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
100pub struct Viewport {
101 pub zoom: f64,
103
104 pub x: f64,
106
107 pub y: f64,
109}
110
111impl VisualLayout {
112 pub fn new() -> Self {
114 Self {
115 nodes: Vec::new(),
116 edges: Vec::new(),
117 viewport: None,
118 }
119 }
120
121 pub fn add_node(mut self, node: VisualNode) -> Self {
123 self.nodes.push(node);
124 self
125 }
126
127 pub fn add_edge(mut self, edge: VisualEdge) -> Self {
129 self.edges.push(edge);
130 self
131 }
132
133 pub fn with_viewport(mut self, viewport: Viewport) -> Self {
135 self.viewport = Some(viewport);
136 self
137 }
138
139 pub fn to_react_flow_json(&self) -> serde_json::Value {
143 serde_json::json!({
144 "nodes": self.nodes.iter().map(|n| {
145 let mut node_data = serde_json::Map::new();
146 node_data.insert("label".to_string(), serde_json::Value::String(n.label.clone()));
147 for (k, v) in &n.data {
148 node_data.insert(k.clone(), v.clone());
149 }
150 serde_json::json!({
151 "id": n.id,
152 "type": n.node_type,
153 "position": {
154 "x": n.position_x,
155 "y": n.position_y
156 },
157 "data": node_data,
158 "style": n.style,
159 "width": n.width,
160 "height": n.height
161 })
162 }).collect::<Vec<_>>(),
163 "edges": self.edges.iter().map(|e| {
164 serde_json::json!({
165 "id": e.id,
166 "source": e.source,
167 "target": e.target,
168 "label": e.label,
169 "type": e.edge_type,
170 "animated": e.animated,
171 "style": e.style,
172 "data": e.data
173 })
174 }).collect::<Vec<_>>(),
175 "viewport": self.viewport.as_ref().map(|v| {
176 serde_json::json!({
177 "zoom": v.zoom,
178 "x": v.x,
179 "y": v.y
180 })
181 })
182 })
183 }
184
185 pub fn from_react_flow_json(value: &serde_json::Value) -> Result<Self, serde_json::Error> {
189 let empty_vec: Vec<serde_json::Value> = Vec::new();
190 let nodes = value
191 .get("nodes")
192 .and_then(|n| n.as_array())
193 .unwrap_or(&empty_vec)
194 .iter()
195 .map(|n| {
196 let empty_map = serde_json::Map::new();
197 let position = n.get("position").and_then(|p| p.as_object()).unwrap_or(&empty_map);
198 let data = n.get("data").and_then(|d| d.as_object()).unwrap_or(&empty_map);
199 let style = n.get("style").and_then(|s| s.as_object()).unwrap_or(&empty_map);
200
201 VisualNode {
202 id: n.get("id").and_then(|i| i.as_str()).unwrap_or("").to_string(),
203 node_type: n
204 .get("type")
205 .and_then(|t| t.as_str())
206 .unwrap_or("state")
207 .to_string(),
208 position_x: position.get("x").and_then(|x| x.as_f64()).unwrap_or(0.0),
209 position_y: position.get("y").and_then(|y| y.as_f64()).unwrap_or(0.0),
210 width: n.get("width").and_then(|w| w.as_f64()).unwrap_or(150.0),
211 height: n.get("height").and_then(|h| h.as_f64()).unwrap_or(40.0),
212 label: data.get("label").and_then(|l| l.as_str()).unwrap_or("").to_string(),
213 style: style.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
214 data: data.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
215 }
216 })
217 .collect();
218
219 let empty_vec: Vec<serde_json::Value> = Vec::new();
220 let edges = value
221 .get("edges")
222 .and_then(|e| e.as_array())
223 .unwrap_or(&empty_vec)
224 .iter()
225 .map(|e| {
226 let empty_map = serde_json::Map::new();
227 let style = e.get("style").and_then(|s| s.as_object()).unwrap_or(&empty_map);
228 let data = e.get("data").and_then(|d| d.as_object()).unwrap_or(&empty_map);
229
230 VisualEdge {
231 id: e.get("id").and_then(|i| i.as_str()).unwrap_or("").to_string(),
232 source: e.get("source").and_then(|s| s.as_str()).unwrap_or("").to_string(),
233 target: e.get("target").and_then(|t| t.as_str()).unwrap_or("").to_string(),
234 label: e.get("label").and_then(|l| l.as_str()).map(|s| s.to_string()),
235 edge_type: e
236 .get("type")
237 .and_then(|t| t.as_str())
238 .unwrap_or("default")
239 .to_string(),
240 animated: e.get("animated").and_then(|a| a.as_bool()).unwrap_or(false),
241 style: style.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
242 data: data.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
243 }
244 })
245 .collect();
246
247 let viewport = value.get("viewport").and_then(|v| serde_json::from_value(v.clone()).ok());
248
249 Ok(Self {
250 nodes,
251 edges,
252 viewport,
253 })
254 }
255}
256
257impl Default for VisualLayout {
258 fn default() -> Self {
259 Self::new()
260 }
261}
262
263fn default_width() -> f64 {
264 150.0
265}
266
267fn default_height() -> f64 {
268 40.0
269}
270
271fn default_edge_type() -> String {
272 "default".to_string()
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278
279 #[test]
280 fn test_visual_layout_creation() {
281 let layout = VisualLayout::new()
282 .add_node(VisualNode {
283 id: "state1".to_string(),
284 node_type: "state".to_string(),
285 position_x: 100.0,
286 position_y: 200.0,
287 width: 150.0,
288 height: 40.0,
289 label: "Pending".to_string(),
290 style: HashMap::new(),
291 data: HashMap::new(),
292 })
293 .add_edge(VisualEdge {
294 id: "edge1".to_string(),
295 source: "state1".to_string(),
296 target: "state2".to_string(),
297 label: Some("condition".to_string()),
298 edge_type: "transition".to_string(),
299 animated: false,
300 style: HashMap::new(),
301 data: HashMap::new(),
302 });
303
304 assert_eq!(layout.nodes.len(), 1);
305 assert_eq!(layout.edges.len(), 1);
306 }
307
308 #[test]
309 fn test_react_flow_conversion() {
310 let layout = VisualLayout::new().add_node(VisualNode {
311 id: "state1".to_string(),
312 node_type: "state".to_string(),
313 position_x: 100.0,
314 position_y: 200.0,
315 width: 150.0,
316 height: 40.0,
317 label: "Pending".to_string(),
318 style: HashMap::new(),
319 data: HashMap::new(),
320 });
321
322 let json = layout.to_react_flow_json();
323 assert!(json.get("nodes").is_some());
324 assert!(json.get("edges").is_some());
325
326 let parsed = VisualLayout::from_react_flow_json(&json).unwrap();
328 assert_eq!(parsed.nodes.len(), 1);
329 assert_eq!(parsed.nodes[0].id, "state1");
330 }
331}