1use 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#[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 let root = path.map_or_else(
40 || std::env::current_dir().unwrap_or_default(),
41 PathBuf::from,
42 );
43
44 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 let dir = match direction.to_lowercase().as_str() {
53 "tb" | "topbottom" | "top-bottom" => Direction::TopToBottom,
54 _ => Direction::LeftToRight,
55 };
56
57 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 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 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 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
147fn 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
189fn 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}