Skip to main content

stmo_cli/commands/
datasources.rs

1#![allow(clippy::missing_errors_doc)]
2
3use super::OutputFormat;
4use crate::api::RedashClient;
5use crate::models::DataSource;
6use anyhow::Result;
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
67            .get_data_source_schema(data_source_id, refresh_schema)
68            .await
69        {
70            Ok(s) => Some(s),
71            Err(e) => {
72                eprintln!("Error fetching schema: {e:#}");
73                None
74            }
75        }
76    } else {
77        None
78    };
79
80    match format {
81        OutputFormat::Json => {
82            let output = serde_json::json!({
83                "data_source": ds,
84                "schema": schema,
85            });
86            let json = serde_json::to_string_pretty(&output)?;
87            println!("{json}");
88        }
89        OutputFormat::Table => {
90            println!("=== DATA SOURCE: {} ===\n", ds.name);
91            println!("ID:          {}", ds.id);
92            println!("Type:        {}", ds.ds_type);
93
94            if let Some(syntax) = &ds.syntax {
95                println!("Syntax:      {syntax}");
96            }
97
98            if let Some(description) = &ds.description {
99                println!("Description: {description}");
100            }
101
102            if ds.paused != 0 {
103                println!("Status:      PAUSED");
104                if let Some(reason) = &ds.pause_reason {
105                    println!("Reason:      {reason}");
106                }
107            } else {
108                println!("Status:      Active");
109            }
110
111            if ds.view_only {
112                println!("Access:      View-only");
113            }
114
115            if let Some(queue) = &ds.queue_name {
116                println!("Queue:       {queue}");
117            }
118
119            if show_schema {
120                if let Some(schema) = schema {
121                    println!("\n=== SCHEMA ({} tables) ===\n", schema.schema.len());
122
123                    for table in &schema.schema {
124                        println!("\nTable: {} ({} columns)", table.name, table.columns.len());
125                        println!("  {:<40} Type", "Column");
126                        println!("  {}", "-".repeat(60));
127
128                        for column in &table.columns {
129                            println!("  {:<40} {}", column.name, column.column_type);
130                        }
131                    }
132                } else {
133                    eprintln!("\nNote: Schema could not be fetched. See error above for details.");
134                }
135            } else {
136                println!("\nUse --schema flag to view table schema.");
137            }
138        }
139    }
140
141    Ok(())
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    fn create_test_datasource(
149        id: u64,
150        name: &str,
151        ds_type: &str,
152        paused: u8,
153        view_only: bool,
154        description: Option<String>,
155    ) -> DataSource {
156        DataSource {
157            id,
158            name: name.to_string(),
159            ds_type: ds_type.to_string(),
160            syntax: Some("sql".to_string()),
161            description,
162            paused,
163            pause_reason: None,
164            view_only,
165            queue_name: None,
166            scheduled_queue_name: None,
167            groups: None,
168            options: None,
169        }
170    }
171
172    #[test]
173    fn test_build_status_string_no_status() {
174        let ds = create_test_datasource(1, "Test DB", "bigquery", 0, false, None);
175        assert_eq!(build_status_string(&ds), "");
176    }
177
178    #[test]
179    fn test_build_status_string_paused() {
180        let ds = create_test_datasource(1, "Test DB", "bigquery", 1, false, None);
181        assert_eq!(build_status_string(&ds), "[paused]");
182    }
183
184    #[test]
185    fn test_build_status_string_view_only() {
186        let ds = create_test_datasource(1, "Test DB", "bigquery", 0, true, None);
187        assert_eq!(build_status_string(&ds), "[view-only]");
188    }
189
190    #[test]
191    fn test_build_status_string_deprecated() {
192        let ds = create_test_datasource(
193            1,
194            "Test DB",
195            "bigquery",
196            0,
197            false,
198            Some("This is deprecated".to_string()),
199        );
200        assert_eq!(build_status_string(&ds), "[deprecated]");
201    }
202
203    #[test]
204    fn test_build_status_string_multiple() {
205        let ds = create_test_datasource(
206            1,
207            "Test DB",
208            "bigquery",
209            1,
210            true,
211            Some("This is deprecated".to_string()),
212        );
213        assert_eq!(build_status_string(&ds), "[paused, view-only, deprecated]");
214    }
215
216    #[test]
217    fn test_build_status_string_deprecated_case_insensitive() {
218        let ds1 = create_test_datasource(
219            1,
220            "Test DB",
221            "bigquery",
222            0,
223            false,
224            Some("DEPRECATED: do not use".to_string()),
225        );
226        assert_eq!(build_status_string(&ds1), "[deprecated]");
227
228        let ds2 = create_test_datasource(
229            2,
230            "Test DB",
231            "bigquery",
232            0,
233            false,
234            Some("Deprecated API".to_string()),
235        );
236        assert_eq!(build_status_string(&ds2), "[deprecated]");
237    }
238
239    #[test]
240    fn test_build_status_string_no_description() {
241        let ds = create_test_datasource(1, "Test DB", "bigquery", 0, true, None);
242        assert_eq!(build_status_string(&ds), "[view-only]");
243    }
244
245    #[test]
246    fn test_build_status_string_description_no_deprecated() {
247        let ds = create_test_datasource(
248            1,
249            "Test DB",
250            "bigquery",
251            0,
252            false,
253            Some("Some other description".to_string()),
254        );
255        assert_eq!(build_status_string(&ds), "");
256    }
257
258    #[test]
259    fn test_output_format_from_str() {
260        assert!(matches!(
261            "json".parse::<OutputFormat>().unwrap(),
262            OutputFormat::Json
263        ));
264        assert!(matches!(
265            "JSON".parse::<OutputFormat>().unwrap(),
266            OutputFormat::Json
267        ));
268        assert!(matches!(
269            "table".parse::<OutputFormat>().unwrap(),
270            OutputFormat::Table
271        ));
272        assert!(matches!(
273            "TABLE".parse::<OutputFormat>().unwrap(),
274            OutputFormat::Table
275        ));
276        assert!("invalid".parse::<OutputFormat>().is_err());
277        assert!("csv".parse::<OutputFormat>().is_err());
278    }
279}