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};
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(&root, &config)
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                highlight_cross_language: highlight_cross,
83                max_depth: None,
84                root_nodes: HashSet::new(),
85                direction: dir,
86                show_details,
87                show_edge_labels: show_labels,
88            };
89            let exporter = UnifiedDotExporter::with_config(&snapshot, config);
90            exporter.export()
91        }
92        "d2" => {
93            let config = D2Config {
94                filter_languages,
95                filter_edges,
96                highlight_cross_language: highlight_cross,
97                show_details,
98                show_edge_labels: show_labels,
99                direction: dir,
100            };
101            let exporter = UnifiedD2Exporter::with_config(&snapshot, config);
102            exporter.export()
103        }
104        "mermaid" | "md" => {
105            let config = MermaidConfig {
106                filter_languages,
107                filter_edges,
108                highlight_cross_language: highlight_cross,
109                show_edge_labels: show_labels,
110                direction: dir,
111            };
112            let exporter = UnifiedMermaidExporter::with_config(&snapshot, config);
113            exporter.export()
114        }
115        "json" => {
116            let config = JsonConfig {
117                include_details: show_details,
118                include_edge_metadata: show_labels,
119            };
120            let exporter = UnifiedJsonExporter::with_config(&snapshot, config);
121            serde_json::to_string_pretty(&exporter.export()).context("Failed to serialize JSON")?
122        }
123        _ => {
124            return Err(anyhow::anyhow!(
125                "Unknown format: {format}. Use: dot, d2, mermaid, json"
126            ));
127        }
128    };
129
130    // Write output
131    if let Some(file_path) = output_file {
132        let mut file = File::create(file_path)
133            .with_context(|| format!("Failed to create output file: {file_path}"))?;
134        file.write_all(output.as_bytes())
135            .context("Failed to write output")?;
136        streams.write_diagnostic(&format!("Exported to {file_path}"))?;
137    } else {
138        streams.write_result(&output)?;
139    }
140
141    Ok(())
142}
143
144/// Parse a language string to Language enum
145fn parse_language(s: &str) -> Option<Language> {
146    match s.to_lowercase().as_str() {
147        "rust" | "rs" => Some(Language::Rust),
148        "javascript" | "js" => Some(Language::JavaScript),
149        "typescript" | "ts" => Some(Language::TypeScript),
150        "python" | "py" => Some(Language::Python),
151        "go" => Some(Language::Go),
152        "java" => Some(Language::Java),
153        "ruby" | "rb" => Some(Language::Ruby),
154        "php" => Some(Language::Php),
155        "cpp" | "c++" => Some(Language::Cpp),
156        "c" => Some(Language::C),
157        "swift" => Some(Language::Swift),
158        "kotlin" | "kt" => Some(Language::Kotlin),
159        "scala" => Some(Language::Scala),
160        "sql" => Some(Language::Sql),
161        "shell" | "bash" | "sh" => Some(Language::Shell),
162        "lua" => Some(Language::Lua),
163        "perl" | "pl" => Some(Language::Perl),
164        "dart" => Some(Language::Dart),
165        "groovy" => Some(Language::Groovy),
166        "css" => Some(Language::Css),
167        "elixir" | "ex" => Some(Language::Elixir),
168        "r" => Some(Language::R),
169        "haskell" | "hs" => Some(Language::Haskell),
170        "html" => Some(Language::Html),
171        "svelte" => Some(Language::Svelte),
172        "vue" => Some(Language::Vue),
173        "zig" => Some(Language::Zig),
174        "terraform" | "tf" => Some(Language::Terraform),
175        "puppet" => Some(Language::Puppet),
176        "apex" => Some(Language::Apex),
177        "abap" => Some(Language::Abap),
178        "csharp" | "cs" | "c#" => Some(Language::CSharp),
179        "http" => Some(Language::Http),
180        "plsql" | "pl/sql" | "oracle" => Some(Language::Plsql),
181        "servicenow" | "xanadu" => Some(Language::ServiceNow),
182        _ => None,
183    }
184}
185
186/// Parse an edge filter string
187fn parse_edge_filter(s: &str) -> Option<EdgeFilter> {
188    match s.to_lowercase().as_str() {
189        "calls" | "call" => Some(EdgeFilter::Calls),
190        "imports" | "import" => Some(EdgeFilter::Imports),
191        "exports" | "export" => Some(EdgeFilter::Exports),
192        "references" | "reference" | "refs" => Some(EdgeFilter::References),
193        "inherits" | "inherit" | "extends" => Some(EdgeFilter::Inherits),
194        "implements" | "implement" => Some(EdgeFilter::Implements),
195        "ffi" | "fficall" => Some(EdgeFilter::FfiCall),
196        "http" | "httprequest" => Some(EdgeFilter::HttpRequest),
197        "db" | "dbquery" | "database" => Some(EdgeFilter::DbQuery),
198        _ => None,
199    }
200}