flowscope_cli/
cli.rs

1//! CLI argument parsing using clap.
2
3use clap::{Parser, ValueEnum};
4use std::path::PathBuf;
5
6/// FlowScope - SQL lineage analyzer
7#[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    /// SQL files to analyze (reads from stdin if none provided)
13    #[arg(value_name = "FILES")]
14    pub files: Vec<PathBuf>,
15
16    /// SQL dialect
17    #[arg(short, long, default_value = "generic", value_enum)]
18    pub dialect: DialectArg,
19
20    /// Output format
21    #[arg(short, long, default_value = "table", value_enum)]
22    pub format: OutputFormat,
23
24    /// Schema DDL file for table/column resolution
25    #[arg(short, long, value_name = "FILE")]
26    pub schema: Option<PathBuf>,
27
28    /// Database connection URL for live schema introspection
29    /// (e.g., postgres://user:pass@host/db, mysql://..., sqlite://...)
30    #[cfg(feature = "metadata-provider")]
31    #[arg(long, value_name = "URL")]
32    pub metadata_url: Option<String>,
33
34    /// Schema name to filter when using --metadata-url
35    /// (e.g., 'public' for PostgreSQL, database name for MySQL)
36    #[cfg(feature = "metadata-provider")]
37    #[arg(long, value_name = "SCHEMA")]
38    pub metadata_schema: Option<String>,
39
40    /// Output file (defaults to stdout)
41    #[arg(short, long, value_name = "FILE")]
42    pub output: Option<PathBuf>,
43
44    /// Project name used for default export filenames
45    #[arg(long, default_value = "lineage")]
46    pub project_name: String,
47
48    /// Schema name to prefix DuckDB SQL export
49    #[arg(long, value_name = "SCHEMA")]
50    pub export_schema: Option<String>,
51
52    /// Graph detail level for mermaid output
53    #[arg(short, long, default_value = "table", value_enum)]
54    pub view: ViewMode,
55
56    /// Suppress warnings on stderr
57    #[arg(short, long)]
58    pub quiet: bool,
59
60    /// Compact JSON output (no pretty-printing)
61    #[arg(short, long)]
62    pub compact: bool,
63
64    /// Template mode for preprocessing SQL (jinja or dbt)
65    #[cfg(feature = "templating")]
66    #[arg(long, value_enum)]
67    pub template: Option<TemplateArg>,
68
69    /// Template variable in KEY=VALUE format (can be repeated)
70    #[cfg(feature = "templating")]
71    #[arg(long = "template-var", value_name = "KEY=VALUE")]
72    pub template_vars: Vec<String>,
73
74    /// Start HTTP server with embedded web UI
75    #[cfg(feature = "serve")]
76    #[arg(long)]
77    pub serve: bool,
78
79    /// Port for HTTP server (default: 3000)
80    #[cfg(feature = "serve")]
81    #[arg(long, default_value = "3000")]
82    pub port: u16,
83
84    /// Directories to watch for SQL files (can be repeated)
85    #[cfg(feature = "serve")]
86    #[arg(long, value_name = "DIR")]
87    pub watch: Vec<PathBuf>,
88
89    /// Open browser automatically when server starts
90    #[cfg(feature = "serve")]
91    #[arg(long)]
92    pub open: bool,
93}
94
95/// SQL dialect options
96#[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/// Output format options
134#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
135pub enum OutputFormat {
136    /// Human-readable table format
137    Table,
138    /// JSON output
139    Json,
140    /// Mermaid diagram
141    Mermaid,
142    /// HTML report
143    Html,
144    /// DuckDB SQL export
145    Sql,
146    /// CSV archive (zip)
147    Csv,
148    /// XLSX export
149    Xlsx,
150    /// DuckDB database file
151    Duckdb,
152}
153
154/// Template mode for SQL preprocessing
155#[cfg(feature = "templating")]
156#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
157pub enum TemplateArg {
158    /// Plain Jinja2 templating
159    Jinja,
160    /// dbt-style templating with builtin macros
161    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/// Graph detail level for visualization
175#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
176pub enum ViewMode {
177    /// Script/file level relationships
178    Script,
179    /// Table level lineage (default)
180    Table,
181    /// Column level lineage
182    Column,
183    /// Hybrid view (scripts + tables)
184    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}