hydro_lang/viz/
api.rs

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