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}