bnto_core/editor/
convert.rs1use std::collections::HashMap;
4
5use crate::definition::{Definition, Edge, Metadata, Position};
6use crate::pipeline::{IterationMode, PipelineSettings};
7use crate::{FORMAT_VERSION, definition};
8
9use super::types::{EditorModel, EditorNode, EditorSource};
10
11impl EditorModel {
12 pub fn from_definition(def: &Definition, source: EditorSource) -> Self {
14 let nodes: Vec<EditorNode> = def
15 .nodes
16 .as_ref()
17 .map(|defs| {
18 defs.iter()
19 .map(|d| EditorNode {
20 id: d.id.clone(),
21 node_type: d.node_type.clone(),
22 label: d.name.clone(),
23 params: json_to_params(&d.parameters),
24 expanded: false,
25 })
26 .collect()
27 })
28 .unwrap_or_default();
29
30 let selected_index = if nodes.is_empty() { None } else { Some(0) };
31
32 Self {
33 recipe_name: def.name.clone(),
34 recipe_description: def.metadata.description.clone().unwrap_or_default(),
35 nodes,
36 selected_index,
37 dirty: false,
38 undo_stack: Vec::new(),
39 redo_stack: Vec::new(),
40 source,
41 }
42 }
43
44 pub fn to_definition(&self) -> Definition {
46 let child_defs: Vec<Definition> = self
47 .nodes
48 .iter()
49 .enumerate()
50 .map(|(i, node)| Definition {
51 id: node.id.clone(),
52 node_type: node.node_type.clone(),
53 version: FORMAT_VERSION.to_string(),
54 parent_id: None,
55 name: node.label.clone(),
56 position: Position {
57 x: 0.0,
58 y: (i as f64) * 100.0,
59 },
60 metadata: Metadata::default(),
61 parameters: params_to_json(&node.params),
62 input_ports: vec![],
63 output_ports: vec![],
64 nodes: None,
65 edges: None,
66 settings: None,
67 requires: Vec::new(),
68 fields: std::collections::BTreeMap::new(),
69 })
70 .collect();
71
72 let edges: Vec<Edge> = child_defs
73 .windows(2)
74 .enumerate()
75 .map(|(i, pair)| Edge {
76 id: format!("e{}", i + 1),
77 source: pair[0].id.clone(),
78 target: pair[1].id.clone(),
79 source_handle: None,
80 target_handle: None,
81 })
82 .collect();
83
84 Definition {
85 id: slug_from_name(&self.recipe_name),
86 node_type: "group".to_string(),
87 version: FORMAT_VERSION.to_string(),
88 parent_id: None,
89 name: self.recipe_name.clone(),
90 position: Position { x: 0.0, y: 0.0 },
91 metadata: Metadata {
92 description: if self.recipe_description.is_empty() {
93 None
94 } else {
95 Some(self.recipe_description.clone())
96 },
97 ..Default::default()
98 },
99 parameters: definition::default_parameters(),
100 input_ports: vec![],
101 output_ports: vec![],
102 nodes: Some(child_defs),
103 edges: Some(edges),
104 settings: Some(PipelineSettings {
105 iteration: IterationMode::Auto,
106 }),
107 requires: Vec::new(),
108 fields: std::collections::BTreeMap::new(),
109 }
110 }
111}
112
113pub(crate) fn json_to_params(value: &serde_json::Value) -> HashMap<String, serde_json::Value> {
115 match value.as_object() {
116 Some(map) => map.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
117 None => HashMap::new(),
118 }
119}
120
121pub(crate) fn params_to_json(params: &HashMap<String, serde_json::Value>) -> serde_json::Value {
123 let map: serde_json::Map<String, serde_json::Value> =
124 params.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
125 serde_json::Value::Object(map)
126}
127
128pub(crate) fn slug_from_name(name: &str) -> String {
130 name.to_lowercase()
131 .chars()
132 .map(|c| if c.is_alphanumeric() { c } else { '-' })
133 .collect::<String>()
134 .split('-')
135 .filter(|s| !s.is_empty())
136 .collect::<Vec<_>>()
137 .join("-")
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn slug_from_name_produces_valid_slug() {
146 assert_eq!(slug_from_name("My Great Recipe"), "my-great-recipe");
147 assert_eq!(slug_from_name(""), "");
148 assert_eq!(slug_from_name("hello"), "hello");
149 assert_eq!(slug_from_name("A B"), "a-b");
150 assert_eq!(slug_from_name("Compress & Resize!"), "compress-resize");
151 }
152
153 #[test]
154 fn json_to_params_handles_object() {
155 use serde_json::json;
156 let val = json!({"quality": 80, "format": "webp"});
157 let params = json_to_params(&val);
158 assert_eq!(params.get("quality"), Some(&json!(80)));
159 assert_eq!(params.get("format"), Some(&json!("webp")));
160 }
161
162 #[test]
163 fn json_to_params_handles_non_object() {
164 use serde_json::json;
165 let val = json!(42);
166 let params = json_to_params(&val);
167 assert!(params.is_empty());
168 }
169}