hydro_lang/graph/
api.rs

1use std::error::Error;
2
3use crate::graph::render::HydroWriteConfig;
4use crate::ir::HydroRoot;
5
6/// Graph generation API for built flows
7pub struct GraphApi<'a> {
8    ir: &'a [HydroRoot],
9    process_id_name: &'a [(usize, String)],
10    cluster_id_name: &'a [(usize, String)],
11    external_id_name: &'a [(usize, String)],
12}
13
14/// Graph output format
15#[derive(Debug, Clone, Copy)]
16pub enum GraphFormat {
17    Mermaid,
18    Dot,
19    ReactFlow,
20}
21
22impl GraphFormat {
23    fn file_extension(self) -> &'static str {
24        match self {
25            GraphFormat::Mermaid => "mmd",
26            GraphFormat::Dot => "dot",
27            GraphFormat::ReactFlow => "json",
28        }
29    }
30
31    fn browser_message(self) -> &'static str {
32        match self {
33            GraphFormat::Mermaid => "Opening Mermaid graph in browser...",
34            GraphFormat::Dot => "Opening Graphviz/DOT graph in browser...",
35            GraphFormat::ReactFlow => "Opening ReactFlow graph in browser...",
36        }
37    }
38}
39
40impl<'a> GraphApi<'a> {
41    pub fn new(
42        ir: &'a [HydroRoot],
43        process_id_name: &'a [(usize, String)],
44        cluster_id_name: &'a [(usize, String)],
45        external_id_name: &'a [(usize, String)],
46    ) -> Self {
47        Self {
48            ir,
49            process_id_name,
50            cluster_id_name,
51            external_id_name,
52        }
53    }
54
55    /// Convert configuration options to HydroWriteConfig
56    fn to_hydro_config(
57        &self,
58        show_metadata: bool,
59        show_location_groups: bool,
60        use_short_labels: bool,
61    ) -> HydroWriteConfig {
62        HydroWriteConfig {
63            show_metadata,
64            show_location_groups,
65            use_short_labels,
66            process_id_name: self.process_id_name.to_vec(),
67            cluster_id_name: self.cluster_id_name.to_vec(),
68            external_id_name: self.external_id_name.to_vec(),
69        }
70    }
71
72    /// Generate graph content as string
73    fn render_graph_to_string(&self, format: GraphFormat, config: &HydroWriteConfig) -> String {
74        match format {
75            GraphFormat::Mermaid => crate::graph::render::render_hydro_ir_mermaid(self.ir, config),
76            GraphFormat::Dot => crate::graph::render::render_hydro_ir_dot(self.ir, config),
77            GraphFormat::ReactFlow => {
78                crate::graph::render::render_hydro_ir_reactflow(self.ir, config)
79            }
80        }
81    }
82
83    /// Open graph in browser
84    fn open_graph_in_browser(
85        &self,
86        format: GraphFormat,
87        config: HydroWriteConfig,
88    ) -> Result<(), Box<dyn Error>> {
89        match format {
90            GraphFormat::Mermaid => Ok(crate::graph::debug::open_mermaid(self.ir, Some(config))?),
91            GraphFormat::Dot => Ok(crate::graph::debug::open_dot(self.ir, Some(config))?),
92            GraphFormat::ReactFlow => Ok(crate::graph::debug::open_reactflow_browser(
93                self.ir,
94                None,
95                Some(config),
96            )?),
97        }
98    }
99
100    /// Generic method to open graph in browser
101    fn open_browser(
102        &self,
103        format: GraphFormat,
104        show_metadata: bool,
105        show_location_groups: bool,
106        use_short_labels: bool,
107        message_handler: Option<&dyn Fn(&str)>,
108    ) -> Result<(), Box<dyn Error>> {
109        let default_handler = |msg: &str| println!("{}", msg);
110        let handler = message_handler.unwrap_or(&default_handler);
111
112        let config = self.to_hydro_config(show_metadata, show_location_groups, use_short_labels);
113
114        handler(format.browser_message());
115        self.open_graph_in_browser(format, config)?;
116        Ok(())
117    }
118
119    /// Generate and save graph to file
120    fn write_graph_to_file(
121        &self,
122        format: GraphFormat,
123        filename: &str,
124        show_metadata: bool,
125        show_location_groups: bool,
126        use_short_labels: bool,
127    ) -> Result<(), Box<dyn Error>> {
128        let config = self.to_hydro_config(show_metadata, show_location_groups, use_short_labels);
129        let content = self.render_graph_to_string(format, &config);
130        std::fs::write(filename, content)?;
131        println!("Generated: {}", filename);
132        Ok(())
133    }
134
135    /// Generate mermaid graph as string
136    pub fn mermaid_to_string(
137        &self,
138        show_metadata: bool,
139        show_location_groups: bool,
140        use_short_labels: bool,
141    ) -> String {
142        let config = self.to_hydro_config(show_metadata, show_location_groups, use_short_labels);
143        self.render_graph_to_string(GraphFormat::Mermaid, &config)
144    }
145
146    /// Generate DOT graph as string
147    pub fn dot_to_string(
148        &self,
149        show_metadata: bool,
150        show_location_groups: bool,
151        use_short_labels: bool,
152    ) -> String {
153        let config = self.to_hydro_config(show_metadata, show_location_groups, use_short_labels);
154        self.render_graph_to_string(GraphFormat::Dot, &config)
155    }
156
157    /// Generate ReactFlow graph as string
158    pub fn reactflow_to_string(
159        &self,
160        show_metadata: bool,
161        show_location_groups: bool,
162        use_short_labels: bool,
163    ) -> String {
164        let config = self.to_hydro_config(show_metadata, show_location_groups, use_short_labels);
165        self.render_graph_to_string(GraphFormat::ReactFlow, &config)
166    }
167
168    /// Write mermaid graph to file
169    pub fn mermaid_to_file(
170        &self,
171        filename: &str,
172        show_metadata: bool,
173        show_location_groups: bool,
174        use_short_labels: bool,
175    ) -> Result<(), Box<dyn Error>> {
176        self.write_graph_to_file(
177            GraphFormat::Mermaid,
178            filename,
179            show_metadata,
180            show_location_groups,
181            use_short_labels,
182        )
183    }
184
185    /// Write DOT graph to file
186    pub fn dot_to_file(
187        &self,
188        filename: &str,
189        show_metadata: bool,
190        show_location_groups: bool,
191        use_short_labels: bool,
192    ) -> Result<(), Box<dyn Error>> {
193        self.write_graph_to_file(
194            GraphFormat::Dot,
195            filename,
196            show_metadata,
197            show_location_groups,
198            use_short_labels,
199        )
200    }
201
202    /// Write ReactFlow graph to file
203    pub fn reactflow_to_file(
204        &self,
205        filename: &str,
206        show_metadata: bool,
207        show_location_groups: bool,
208        use_short_labels: bool,
209    ) -> Result<(), Box<dyn Error>> {
210        self.write_graph_to_file(
211            GraphFormat::ReactFlow,
212            filename,
213            show_metadata,
214            show_location_groups,
215            use_short_labels,
216        )
217    }
218
219    /// Open mermaid graph in browser
220    pub fn mermaid_to_browser(
221        &self,
222        show_metadata: bool,
223        show_location_groups: bool,
224        use_short_labels: bool,
225        message_handler: Option<&dyn Fn(&str)>,
226    ) -> Result<(), Box<dyn Error>> {
227        self.open_browser(
228            GraphFormat::Mermaid,
229            show_metadata,
230            show_location_groups,
231            use_short_labels,
232            message_handler,
233        )
234    }
235
236    /// Open DOT graph in browser
237    pub fn dot_to_browser(
238        &self,
239        show_metadata: bool,
240        show_location_groups: bool,
241        use_short_labels: bool,
242        message_handler: Option<&dyn Fn(&str)>,
243    ) -> Result<(), Box<dyn Error>> {
244        self.open_browser(
245            GraphFormat::Dot,
246            show_metadata,
247            show_location_groups,
248            use_short_labels,
249            message_handler,
250        )
251    }
252
253    /// Open ReactFlow graph in browser
254    pub fn reactflow_to_browser(
255        &self,
256        show_metadata: bool,
257        show_location_groups: bool,
258        use_short_labels: bool,
259        message_handler: Option<&dyn Fn(&str)>,
260    ) -> Result<(), Box<dyn Error>> {
261        self.open_browser(
262            GraphFormat::ReactFlow,
263            show_metadata,
264            show_location_groups,
265            use_short_labels,
266            message_handler,
267        )
268    }
269
270    /// Generate all graph types and save to files with a given prefix
271    pub fn generate_all_files(
272        &self,
273        prefix: &str,
274        show_metadata: bool,
275        show_location_groups: bool,
276        use_short_labels: bool,
277    ) -> Result<(), Box<dyn Error>> {
278        let label_suffix = if use_short_labels { "_short" } else { "_long" };
279
280        let formats = [
281            GraphFormat::Mermaid,
282            GraphFormat::Dot,
283            GraphFormat::ReactFlow,
284        ];
285
286        for format in formats {
287            let filename = format!(
288                "{}{}_labels.{}",
289                prefix,
290                label_suffix,
291                format.file_extension()
292            );
293            self.write_graph_to_file(
294                format,
295                &filename,
296                show_metadata,
297                show_location_groups,
298                use_short_labels,
299            )?;
300        }
301
302        Ok(())
303    }
304
305    /// Generate graph based on GraphConfig, delegating to the appropriate method
306    #[cfg(feature = "build")]
307    pub fn generate_graph_with_config(
308        &self,
309        config: &crate::graph::config::GraphConfig,
310        message_handler: Option<&dyn Fn(&str)>,
311    ) -> Result<(), Box<dyn Error>> {
312        if let Some(graph_type) = config.graph {
313            let format = match graph_type {
314                crate::graph::config::GraphType::Mermaid => GraphFormat::Mermaid,
315                crate::graph::config::GraphType::Dot => GraphFormat::Dot,
316                crate::graph::config::GraphType::Reactflow => GraphFormat::ReactFlow,
317            };
318
319            self.open_browser(
320                format,
321                !config.no_metadata,
322                !config.no_location_groups,
323                !config.long_labels, // use_short_labels is the inverse of long_labels
324                message_handler,
325            )
326        } else {
327            Ok(())
328        }
329    }
330
331    /// Generate all graph files based on GraphConfig
332    #[cfg(feature = "build")]
333    pub fn generate_all_files_with_config(
334        &self,
335        config: &crate::graph::config::GraphConfig,
336        prefix: &str,
337    ) -> Result<(), Box<dyn Error>> {
338        self.generate_all_files(
339            prefix,
340            !config.no_metadata,
341            !config.no_location_groups,
342            !config.long_labels,
343        )
344    }
345}
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350
351    #[test]
352    fn test_graph_format() {
353        assert_eq!(GraphFormat::Mermaid.file_extension(), "mmd");
354        assert_eq!(GraphFormat::Dot.file_extension(), "dot");
355        assert_eq!(GraphFormat::ReactFlow.file_extension(), "json");
356
357        assert_eq!(
358            GraphFormat::Mermaid.browser_message(),
359            "Opening Mermaid graph in browser..."
360        );
361        assert_eq!(
362            GraphFormat::Dot.browser_message(),
363            "Opening Graphviz/DOT graph in browser..."
364        );
365        assert_eq!(
366            GraphFormat::ReactFlow.browser_message(),
367            "Opening ReactFlow graph in browser..."
368        );
369    }
370
371    #[test]
372    fn test_graph_api_creation() {
373        let ir = vec![];
374        let process_id_name = vec![(0, "test_process".to_string())];
375        let cluster_id_name = vec![];
376        let external_id_name = vec![];
377
378        let api = GraphApi::new(&ir, &process_id_name, &cluster_id_name, &external_id_name);
379
380        // Test config creation
381        let config = api.to_hydro_config(true, true, false);
382        assert!(config.show_metadata);
383        assert!(config.show_location_groups);
384        assert!(!config.use_short_labels);
385        assert_eq!(config.process_id_name.len(), 1);
386        assert_eq!(config.process_id_name[0].1, "test_process");
387    }
388
389    #[test]
390    fn test_string_generation() {
391        let ir = vec![];
392        let process_id_name = vec![(0, "test_process".to_string())];
393        let cluster_id_name = vec![];
394        let external_id_name = vec![];
395
396        let api = GraphApi::new(&ir, &process_id_name, &cluster_id_name, &external_id_name);
397
398        // Test that string generation methods don't panic and return some content
399        let mermaid = api.mermaid_to_string(true, true, false);
400        let dot = api.dot_to_string(true, true, false);
401        let reactflow = api.reactflow_to_string(true, true, false);
402
403        // These should all return non-empty strings
404        assert!(!mermaid.is_empty());
405        assert!(!dot.is_empty());
406        assert!(!reactflow.is_empty());
407    }
408}