1use clap::{Parser, ValueEnum};
4use std::path::PathBuf;
5
6#[derive(Parser, Debug)]
8#[command(name = "flowscope")]
9#[command(about = "Analyze SQL files for data lineage", long_about = None)]
10#[command(version)]
11pub struct Args {
12 #[arg(value_name = "FILES")]
14 pub files: Vec<PathBuf>,
15
16 #[arg(short, long, default_value = "generic", value_enum)]
18 pub dialect: DialectArg,
19
20 #[arg(short, long, default_value = "table", value_enum)]
22 pub format: OutputFormat,
23
24 #[arg(short, long, value_name = "FILE")]
26 pub schema: Option<PathBuf>,
27
28 #[cfg(feature = "metadata-provider")]
31 #[arg(long, value_name = "URL")]
32 pub metadata_url: Option<String>,
33
34 #[cfg(feature = "metadata-provider")]
37 #[arg(long, value_name = "SCHEMA")]
38 pub metadata_schema: Option<String>,
39
40 #[arg(short, long, value_name = "FILE")]
42 pub output: Option<PathBuf>,
43
44 #[arg(long, default_value = "lineage")]
46 pub project_name: String,
47
48 #[arg(long, value_name = "SCHEMA")]
50 pub export_schema: Option<String>,
51
52 #[arg(short, long, default_value = "table", value_enum)]
54 pub view: ViewMode,
55
56 #[arg(short, long)]
58 pub quiet: bool,
59
60 #[arg(short, long)]
62 pub compact: bool,
63
64 #[cfg(feature = "templating")]
66 #[arg(long, value_enum)]
67 pub template: Option<TemplateArg>,
68
69 #[cfg(feature = "templating")]
71 #[arg(long = "template-var", value_name = "KEY=VALUE")]
72 pub template_vars: Vec<String>,
73
74 #[cfg(feature = "serve")]
76 #[arg(long)]
77 pub serve: bool,
78
79 #[cfg(feature = "serve")]
81 #[arg(long, default_value = "3000")]
82 pub port: u16,
83
84 #[cfg(feature = "serve")]
86 #[arg(long, value_name = "DIR")]
87 pub watch: Vec<PathBuf>,
88
89 #[cfg(feature = "serve")]
91 #[arg(long)]
92 pub open: bool,
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
97pub enum DialectArg {
98 Generic,
99 Ansi,
100 Bigquery,
101 Clickhouse,
102 Databricks,
103 Duckdb,
104 Hive,
105 Mssql,
106 Mysql,
107 Postgres,
108 Redshift,
109 Snowflake,
110 Sqlite,
111}
112
113impl From<DialectArg> for flowscope_core::Dialect {
114 fn from(d: DialectArg) -> Self {
115 match d {
116 DialectArg::Generic => flowscope_core::Dialect::Generic,
117 DialectArg::Ansi => flowscope_core::Dialect::Ansi,
118 DialectArg::Bigquery => flowscope_core::Dialect::Bigquery,
119 DialectArg::Clickhouse => flowscope_core::Dialect::Clickhouse,
120 DialectArg::Databricks => flowscope_core::Dialect::Databricks,
121 DialectArg::Duckdb => flowscope_core::Dialect::Duckdb,
122 DialectArg::Hive => flowscope_core::Dialect::Hive,
123 DialectArg::Mssql => flowscope_core::Dialect::Mssql,
124 DialectArg::Mysql => flowscope_core::Dialect::Mysql,
125 DialectArg::Postgres => flowscope_core::Dialect::Postgres,
126 DialectArg::Redshift => flowscope_core::Dialect::Redshift,
127 DialectArg::Snowflake => flowscope_core::Dialect::Snowflake,
128 DialectArg::Sqlite => flowscope_core::Dialect::Sqlite,
129 }
130 }
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
135pub enum OutputFormat {
136 Table,
138 Json,
140 Mermaid,
142 Html,
144 Sql,
146 Csv,
148 Xlsx,
150 Duckdb,
152}
153
154#[cfg(feature = "templating")]
156#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
157pub enum TemplateArg {
158 Jinja,
160 Dbt,
162}
163
164#[cfg(feature = "templating")]
165impl From<TemplateArg> for flowscope_core::TemplateMode {
166 fn from(t: TemplateArg) -> Self {
167 match t {
168 TemplateArg::Jinja => flowscope_core::TemplateMode::Jinja,
169 TemplateArg::Dbt => flowscope_core::TemplateMode::Dbt,
170 }
171 }
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
176pub enum ViewMode {
177 Script,
179 Table,
181 Column,
183 Hybrid,
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 #[test]
192 fn test_dialect_conversion() {
193 let dialect: flowscope_core::Dialect = DialectArg::Postgres.into();
194 assert_eq!(dialect, flowscope_core::Dialect::Postgres);
195 }
196
197 #[test]
198 fn test_parse_minimal_args() {
199 let args = Args::parse_from(["flowscope", "test.sql"]);
200 assert_eq!(args.files.len(), 1);
201 assert_eq!(args.dialect, DialectArg::Generic);
202 assert_eq!(args.format, OutputFormat::Table);
203 assert_eq!(args.project_name, "lineage");
204 assert!(args.export_schema.is_none());
205 }
206
207 #[test]
208 fn test_parse_full_args() {
209 let args = Args::parse_from([
210 "flowscope",
211 "-d",
212 "postgres",
213 "-f",
214 "json",
215 "-s",
216 "schema.sql",
217 "-o",
218 "output.json",
219 "-v",
220 "column",
221 "--quiet",
222 "--compact",
223 "--project-name",
224 "demo",
225 "--export-schema",
226 "lineage",
227 "file1.sql",
228 "file2.sql",
229 ]);
230 assert_eq!(args.dialect, DialectArg::Postgres);
231 assert_eq!(args.format, OutputFormat::Json);
232 assert_eq!(args.schema.unwrap().to_str().unwrap(), "schema.sql");
233 assert_eq!(args.output.unwrap().to_str().unwrap(), "output.json");
234 assert_eq!(args.view, ViewMode::Column);
235 assert_eq!(args.project_name, "demo");
236 assert_eq!(args.export_schema.as_deref(), Some("lineage"));
237 assert!(args.quiet);
238 assert!(args.compact);
239 assert_eq!(args.files.len(), 2);
240 }
241
242 #[cfg(feature = "serve")]
243 #[test]
244 fn test_serve_args_defaults() {
245 let args = Args::parse_from(["flowscope", "--serve"]);
246 assert!(args.serve);
247 assert_eq!(args.port, 3000);
248 assert!(args.watch.is_empty());
249 assert!(!args.open);
250 }
251
252 #[cfg(feature = "serve")]
253 #[test]
254 fn test_serve_args_custom_port() {
255 let args = Args::parse_from(["flowscope", "--serve", "--port", "8080"]);
256 assert!(args.serve);
257 assert_eq!(args.port, 8080);
258 }
259
260 #[cfg(feature = "serve")]
261 #[test]
262 fn test_serve_args_watch_dirs() {
263 let args = Args::parse_from([
264 "flowscope",
265 "--serve",
266 "--watch",
267 "./sql",
268 "--watch",
269 "./queries",
270 ]);
271 assert!(args.serve);
272 assert_eq!(args.watch.len(), 2);
273 assert_eq!(args.watch[0].to_str().unwrap(), "./sql");
274 assert_eq!(args.watch[1].to_str().unwrap(), "./queries");
275 }
276
277 #[cfg(feature = "serve")]
278 #[test]
279 fn test_serve_args_open_browser() {
280 let args = Args::parse_from(["flowscope", "--serve", "--open"]);
281 assert!(args.serve);
282 assert!(args.open);
283 }
284
285 #[cfg(feature = "serve")]
286 #[test]
287 fn test_serve_args_full() {
288 let args = Args::parse_from([
289 "flowscope",
290 "--serve",
291 "--port",
292 "9000",
293 "--watch",
294 "./examples",
295 "--open",
296 "-d",
297 "postgres",
298 ]);
299 assert!(args.serve);
300 assert_eq!(args.port, 9000);
301 assert_eq!(args.watch.len(), 1);
302 assert!(args.open);
303 assert_eq!(args.dialect, DialectArg::Postgres);
304 }
305}