1use 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#[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(&root, &config)
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 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 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
144fn 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
186fn 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}