Skip to main content

stmo_cli/commands/
datasources.rs

1#![allow(clippy::missing_errors_doc)]
2
3use anyhow::Result;
4use crate::api::RedashClient;
5use crate::models::DataSource;
6use super::OutputFormat;
7
8fn build_status_string(ds: &DataSource) -> String {
9    let mut status_parts = Vec::new();
10    if ds.paused != 0 {
11        status_parts.push("paused");
12    }
13    if ds.view_only {
14        status_parts.push("view-only");
15    }
16    if let Some(desc) = &ds.description
17        && desc.to_lowercase().contains("deprecated")
18    {
19        status_parts.push("deprecated");
20    }
21
22    if status_parts.is_empty() {
23        String::new()
24    } else {
25        format!("[{}]", status_parts.join(", "))
26    }
27}
28
29pub async fn list_data_sources(client: &RedashClient, format: OutputFormat) -> Result<()> {
30    let data_sources = client.list_data_sources().await?;
31
32    match format {
33        OutputFormat::Json => {
34            let json = serde_json::to_string_pretty(&data_sources)?;
35            println!("{json}");
36        }
37        OutputFormat::Table => {
38            println!("Fetching data sources from Redash...\n");
39            println!("=== DATA SOURCES ({}) ===\n", data_sources.len());
40            println!("{:<6} {:<40} {:<15} Status", "ID", "Name", "Type");
41            println!("{}", "-".repeat(80));
42
43            for ds in &data_sources {
44                let status = build_status_string(ds);
45                println!("{:<6} {:<40} {:<15} {}", ds.id, ds.name, ds.ds_type, status);
46            }
47
48            println!("\nUse 'stmo-cli data-sources <id>' to view details.");
49            println!("Use 'stmo-cli data-sources <id> --schema' to view table schema.");
50        }
51    }
52
53    Ok(())
54}
55
56pub async fn show_data_source(
57    client: &RedashClient,
58    data_source_id: u64,
59    show_schema: bool,
60    refresh_schema: bool,
61    format: OutputFormat,
62) -> Result<()> {
63    let ds = client.get_data_source(data_source_id).await?;
64
65    let schema = if show_schema {
66        match client.get_data_source_schema(data_source_id, refresh_schema).await {
67            Ok(s) => Some(s),
68            Err(e) => {
69                eprintln!("Error fetching schema: {e:#}");
70                None
71            }
72        }
73    } else {
74        None
75    };
76
77    match format {
78        OutputFormat::Json => {
79            let output = serde_json::json!({
80                "data_source": ds,
81                "schema": schema,
82            });
83            let json = serde_json::to_string_pretty(&output)?;
84            println!("{json}");
85        }
86        OutputFormat::Table => {
87            println!("=== DATA SOURCE: {} ===\n", ds.name);
88            println!("ID:          {}", ds.id);
89            println!("Type:        {}", ds.ds_type);
90
91            if let Some(syntax) = &ds.syntax {
92                println!("Syntax:      {syntax}");
93            }
94
95            if let Some(description) = &ds.description {
96                println!("Description: {description}");
97            }
98
99            if ds.paused != 0 {
100                println!("Status:      PAUSED");
101                if let Some(reason) = &ds.pause_reason {
102                    println!("Reason:      {reason}");
103                }
104            } else {
105                println!("Status:      Active");
106            }
107
108            if ds.view_only {
109                println!("Access:      View-only");
110            }
111
112            if let Some(queue) = &ds.queue_name {
113                println!("Queue:       {queue}");
114            }
115
116            if show_schema {
117                if let Some(schema) = schema {
118                    println!("\n=== SCHEMA ({} tables) ===\n", schema.schema.len());
119
120                    for table in &schema.schema {
121                        println!("\nTable: {} ({} columns)", table.name, table.columns.len());
122                        println!("  {:<40} Type", "Column");
123                        println!("  {}", "-".repeat(60));
124
125                        for column in &table.columns {
126                            println!("  {:<40} {}", column.name, column.column_type);
127                        }
128                    }
129                } else {
130                    eprintln!("\nNote: Schema could not be fetched. See error above for details.");
131                }
132            } else {
133                println!("\nUse --schema flag to view table schema.");
134            }
135        }
136    }
137
138    Ok(())
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    fn create_test_datasource(
146        id: u64,
147        name: &str,
148        ds_type: &str,
149        paused: u8,
150        view_only: bool,
151        description: Option<String>,
152    ) -> DataSource {
153        DataSource {
154            id,
155            name: name.to_string(),
156            ds_type: ds_type.to_string(),
157            syntax: Some("sql".to_string()),
158            description,
159            paused,
160            pause_reason: None,
161            view_only,
162            queue_name: None,
163            scheduled_queue_name: None,
164            groups: None,
165            options: None,
166        }
167    }
168
169    #[test]
170    fn test_build_status_string_no_status() {
171        let ds = create_test_datasource(1, "Test DB", "bigquery", 0, false, None);
172        assert_eq!(build_status_string(&ds), "");
173    }
174
175    #[test]
176    fn test_build_status_string_paused() {
177        let ds = create_test_datasource(1, "Test DB", "bigquery", 1, false, None);
178        assert_eq!(build_status_string(&ds), "[paused]");
179    }
180
181    #[test]
182    fn test_build_status_string_view_only() {
183        let ds = create_test_datasource(1, "Test DB", "bigquery", 0, true, None);
184        assert_eq!(build_status_string(&ds), "[view-only]");
185    }
186
187    #[test]
188    fn test_build_status_string_deprecated() {
189        let ds = create_test_datasource(
190            1,
191            "Test DB",
192            "bigquery",
193            0,
194            false,
195            Some("This is deprecated".to_string()),
196        );
197        assert_eq!(build_status_string(&ds), "[deprecated]");
198    }
199
200    #[test]
201    fn test_build_status_string_multiple() {
202        let ds = create_test_datasource(
203            1,
204            "Test DB",
205            "bigquery",
206            1,
207            true,
208            Some("This is deprecated".to_string()),
209        );
210        assert_eq!(build_status_string(&ds), "[paused, view-only, deprecated]");
211    }
212
213    #[test]
214    fn test_build_status_string_deprecated_case_insensitive() {
215        let ds1 = create_test_datasource(
216            1,
217            "Test DB",
218            "bigquery",
219            0,
220            false,
221            Some("DEPRECATED: do not use".to_string()),
222        );
223        assert_eq!(build_status_string(&ds1), "[deprecated]");
224
225        let ds2 = create_test_datasource(
226            2,
227            "Test DB",
228            "bigquery",
229            0,
230            false,
231            Some("Deprecated API".to_string()),
232        );
233        assert_eq!(build_status_string(&ds2), "[deprecated]");
234    }
235
236    #[test]
237    fn test_build_status_string_no_description() {
238        let ds = create_test_datasource(1, "Test DB", "bigquery", 0, true, None);
239        assert_eq!(build_status_string(&ds), "[view-only]");
240    }
241
242    #[test]
243    fn test_build_status_string_description_no_deprecated() {
244        let ds = create_test_datasource(
245            1,
246            "Test DB",
247            "bigquery",
248            0,
249            false,
250            Some("Some other description".to_string()),
251        );
252        assert_eq!(build_status_string(&ds), "");
253    }
254
255    #[test]
256    fn test_output_format_from_str() {
257        assert!(matches!("json".parse::<OutputFormat>().unwrap(), OutputFormat::Json));
258        assert!(matches!("JSON".parse::<OutputFormat>().unwrap(), OutputFormat::Json));
259        assert!(matches!("table".parse::<OutputFormat>().unwrap(), OutputFormat::Table));
260        assert!(matches!("TABLE".parse::<OutputFormat>().unwrap(), OutputFormat::Table));
261        assert!("invalid".parse::<OutputFormat>().is_err());
262        assert!("csv".parse::<OutputFormat>().is_err());
263    }
264}