Skip to main content

sqry_cli/commands/
export.rs

1//! Export command implementation
2//!
3//! Provides CLI interface for exporting the code graph in various formats.
4
5use crate::args::Cli;
6use crate::commands::graph::loader::{GraphLoadConfig, load_unified_graph_for_cli};
7use crate::output::OutputStreams;
8use anyhow::{Context, Result};
9use sqry_core::graph::Language;
10use sqry_core::visualization::unified::{
11    D2Config, Direction, DotConfig, EdgeFilter, JsonConfig, MermaidConfig, UnifiedD2Exporter,
12    UnifiedDotExporter, UnifiedJsonExporter, UnifiedMermaidExporter,
13};
14use std::collections::HashSet;
15use std::fs::File;
16use std::io::Write;
17use std::path::PathBuf;
18
19/// Run the export command.
20///
21/// # Errors
22/// Returns an error if the graph cannot be loaded or exported.
23#[allow(clippy::too_many_arguments)]
24pub fn run_export(
25    cli: &Cli,
26    path: Option<&str>,
27    format: &str,
28    direction: &str,
29    filter_lang: Option<&str>,
30    filter_edge: Option<&str>,
31    highlight_cross: bool,
32    show_details: bool,
33    show_labels: bool,
34    output_file: Option<&str>,
35) -> Result<()> {
36    let mut streams = OutputStreams::new();
37
38    // Find workspace root
39    let root = path.map_or_else(
40        || std::env::current_dir().unwrap_or_default(),
41        PathBuf::from,
42    );
43
44    // Load unified graph
45    let config = GraphLoadConfig::default();
46    let graph = load_unified_graph_for_cli(&root, &config, cli)
47        .context("Failed to load unified graph. Run 'sqry index' first.")?;
48
49    let snapshot = graph.snapshot();
50
51    // Parse direction
52    let dir = match direction.to_lowercase().as_str() {
53        "tb" | "topbottom" | "top-bottom" => Direction::TopToBottom,
54        _ => Direction::LeftToRight,
55    };
56
57    // Parse language filters
58    let filter_languages: HashSet<Language> = filter_lang
59        .map(|s| {
60            s.split(',')
61                .filter_map(|l| parse_language(l.trim()))
62                .collect()
63        })
64        .unwrap_or_default();
65
66    // Parse edge filters
67    let filter_edges: HashSet<EdgeFilter> = filter_edge
68        .map(|s| {
69            s.split(',')
70                .filter_map(|e| parse_edge_filter(e.trim()))
71                .collect()
72        })
73        .unwrap_or_default();
74
75    // Export based on format
76    let output = match format.to_lowercase().as_str() {
77        "dot" | "graphviz" => {
78            let config = DotConfig {
79                filter_languages,
80                filter_edges,
81                filter_files: HashSet::new(),
82                filter_node_ids: None,
83                highlight_cross_language: highlight_cross,
84                max_depth: None,
85                root_nodes: HashSet::new(),
86                direction: dir,
87                show_details,
88                show_edge_labels: show_labels,
89            };
90            let exporter = UnifiedDotExporter::with_config(&snapshot, config);
91            exporter.export()
92        }
93        "d2" => {
94            let config = D2Config {
95                filter_languages,
96                filter_edges,
97                filter_node_ids: None,
98                highlight_cross_language: highlight_cross,
99                show_details,
100                show_edge_labels: show_labels,
101                direction: dir,
102            };
103            let exporter = UnifiedD2Exporter::with_config(&snapshot, config);
104            exporter.export()
105        }
106        "mermaid" | "md" => {
107            let config = MermaidConfig {
108                filter_languages,
109                filter_edges,
110                highlight_cross_language: highlight_cross,
111                show_edge_labels: show_labels,
112                direction: dir,
113                filter_node_ids: None,
114            };
115            let exporter = UnifiedMermaidExporter::with_config(&snapshot, config);
116            exporter.export()
117        }
118        "json" => {
119            let config = JsonConfig {
120                include_details: show_details,
121                include_edge_metadata: show_labels,
122            };
123            let exporter = UnifiedJsonExporter::with_config(&snapshot, config);
124            serde_json::to_string_pretty(&exporter.export()).context("Failed to serialize JSON")?
125        }
126        _ => {
127            return Err(anyhow::anyhow!(
128                "Unknown format: {format}. Use: dot, d2, mermaid, json"
129            ));
130        }
131    };
132
133    // Write output
134    if let Some(file_path) = output_file {
135        let mut file = File::create(file_path)
136            .with_context(|| format!("Failed to create output file: {file_path}"))?;
137        file.write_all(output.as_bytes())
138            .context("Failed to write output")?;
139        streams.write_diagnostic(&format!("Exported to {file_path}"))?;
140    } else {
141        streams.write_result(&output)?;
142    }
143
144    Ok(())
145}
146
147/// Parse a language string to Language enum
148fn parse_language(s: &str) -> Option<Language> {
149    match s.to_lowercase().as_str() {
150        "rust" | "rs" => Some(Language::Rust),
151        "javascript" | "js" => Some(Language::JavaScript),
152        "typescript" | "ts" => Some(Language::TypeScript),
153        "python" | "py" => Some(Language::Python),
154        "go" => Some(Language::Go),
155        "java" => Some(Language::Java),
156        "ruby" | "rb" => Some(Language::Ruby),
157        "php" => Some(Language::Php),
158        "cpp" | "c++" => Some(Language::Cpp),
159        "c" => Some(Language::C),
160        "swift" => Some(Language::Swift),
161        "kotlin" | "kt" => Some(Language::Kotlin),
162        "scala" => Some(Language::Scala),
163        "sql" => Some(Language::Sql),
164        "shell" | "bash" | "sh" => Some(Language::Shell),
165        "lua" => Some(Language::Lua),
166        "perl" | "pl" => Some(Language::Perl),
167        "dart" => Some(Language::Dart),
168        "groovy" => Some(Language::Groovy),
169        "css" => Some(Language::Css),
170        "elixir" | "ex" => Some(Language::Elixir),
171        "r" => Some(Language::R),
172        "haskell" | "hs" => Some(Language::Haskell),
173        "html" => Some(Language::Html),
174        "svelte" => Some(Language::Svelte),
175        "vue" => Some(Language::Vue),
176        "zig" => Some(Language::Zig),
177        "terraform" | "tf" => Some(Language::Terraform),
178        "puppet" => Some(Language::Puppet),
179        "apex" => Some(Language::Apex),
180        "abap" => Some(Language::Abap),
181        "csharp" | "cs" | "c#" => Some(Language::CSharp),
182        "http" => Some(Language::Http),
183        "plsql" | "pl/sql" | "oracle" => Some(Language::Plsql),
184        "servicenow" | "xanadu" => Some(Language::ServiceNow),
185        _ => None,
186    }
187}
188
189/// Parse an edge filter string
190fn parse_edge_filter(s: &str) -> Option<EdgeFilter> {
191    match s.to_lowercase().as_str() {
192        "calls" | "call" => Some(EdgeFilter::Calls),
193        "imports" | "import" => Some(EdgeFilter::Imports),
194        "exports" | "export" => Some(EdgeFilter::Exports),
195        "references" | "reference" | "refs" => Some(EdgeFilter::References),
196        "inherits" | "inherit" | "extends" => Some(EdgeFilter::Inherits),
197        "implements" | "implement" => Some(EdgeFilter::Implements),
198        "ffi" | "fficall" => Some(EdgeFilter::FfiCall),
199        "http" | "httprequest" => Some(EdgeFilter::HttpRequest),
200        "db" | "dbquery" | "database" => Some(EdgeFilter::DbQuery),
201        _ => None,
202    }
203}