From 74ee995abfad167c4c75a014c541be40c0384666 Mon Sep 17 00:00:00 2001 From: Johan Lorenzo Date: Thu, 21 May 2026 17:39:34 +0200 Subject: [PATCH 1/3] Fix init tests failing when git user identity is not configured --- src/commands/init.rs | 50 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/src/commands/init.rs b/src/commands/init.rs index ca56305..3017a11 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -93,6 +93,36 @@ fn precommit_available() -> bool { .is_ok_and(|output| output.status.success()) } +fn ensure_git_identity(target_dir: &Path) -> Result<()> { + let name_configured = Command::new("git") + .args(["config", "user.name"]) + .current_dir(target_dir) + .output() + .is_ok_and(|o| o.status.success() && !o.stdout.trim_ascii().is_empty()); + + if !name_configured { + let set_name = Command::new("git") + .args(["config", "user.name", "stmo-cli"]) + .current_dir(target_dir) + .status() + .context("Failed to set git user.name")?; + if !set_name.success() { + anyhow::bail!("git config user.name failed"); + } + + let set_email = Command::new("git") + .args(["config", "user.email", "stmo-cli@noreply"]) + .current_dir(target_dir) + .status() + .context("Failed to set git user.email")?; + if !set_email.success() { + anyhow::bail!("git config user.email failed"); + } + } + + Ok(()) +} + fn detect_os() -> &'static str { if cfg!(target_os = "macos") { "macos" @@ -119,6 +149,8 @@ fn setup_git_repo(target_dir: &Path, files_created: bool) -> Result<()> { } } + ensure_git_identity(target_dir)?; + if files_created { println!("⚙ Creating initial commit..."); @@ -270,6 +302,12 @@ mod tests { use tempfile::TempDir; use std::fs; + fn setup_test_repo(dir: &std::path::Path) { + Command::new("git").arg("init").current_dir(dir).status().unwrap(); + Command::new("git").args(["config", "user.name", "Test"]).current_dir(dir).status().unwrap(); + Command::new("git").args(["config", "user.email", "test@test"]).current_dir(dir).status().unwrap(); + } + #[test] fn test_init_creates_all_files() { let temp_dir = TempDir::new().unwrap(); @@ -342,11 +380,7 @@ mod tests { return; } - Command::new("git") - .arg("init") - .current_dir(temp_dir.path()) - .status() - .unwrap(); + setup_test_repo(temp_dir.path()); fs::write(temp_dir.path().join("existing.txt"), "test").unwrap(); Command::new("git") @@ -389,11 +423,7 @@ mod tests { fs::create_dir_all(temp_dir.path().join("dashboards")).unwrap(); fs::write(temp_dir.path().join("dashboards/.gitkeep"), "").unwrap(); - Command::new("git") - .arg("init") - .current_dir(temp_dir.path()) - .status() - .unwrap(); + setup_test_repo(temp_dir.path()); Command::new("git") .args(["add", "."]) .current_dir(temp_dir.path()) From 8c3c874ba5605ff7c607e131bc569fcb88298f1f Mon Sep 17 00:00:00 2001 From: Johan Lorenzo Date: Fri, 22 May 2026 08:44:44 +0200 Subject: [PATCH 2/3] Apply cargo fmt --- src/api.rs | 218 +++++++--- src/commands/archive.rs | 16 +- src/commands/dashboards.rs | 219 ++++++---- src/commands/datasources.rs | 29 +- src/commands/deploy.rs | 50 ++- src/commands/discover.rs | 2 +- src/commands/execute.rs | 162 ++++--- src/commands/fetch.rs | 48 ++- src/commands/init.rs | 32 +- src/commands/mod.rs | 14 +- src/main.rs | 128 ++++-- src/models.rs | 34 +- tests/api_integration.rs | 110 ++--- tests/common/mod.rs | 833 +++++++++++++++++------------------- tests/dashboard_commands.rs | 262 ++++++++---- tests/deploy_commands.rs | 11 +- tests/proxy_tunnel.rs | 3 +- 17 files changed, 1282 insertions(+), 889 deletions(-) diff --git a/src/api.rs b/src/api.rs index ea3d0b0..dec4ce6 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,8 +1,11 @@ #![allow(clippy::missing_errors_doc)] +use crate::models::{ + CreateDashboard, CreateQuery, CreateWidget, Dashboard, DashboardSummary, DashboardsResponse, + DataSource, DataSourceSchema, QueriesResponse, Query, +}; use anyhow::{Context, Result}; use reqwest::{Client, header}; -use crate::models::{CreateDashboard, CreateQuery, CreateWidget, Dashboard, DashboardsResponse, DashboardSummary, DataSource, DataSourceSchema, QueriesResponse, Query}; pub struct RedashClient { client: Client, @@ -23,15 +26,16 @@ impl RedashClient { .build() .context("Failed to build HTTP client")?; - Ok(Self { - client, - base_url, - }) + Ok(Self { client, base_url }) } pub async fn list_my_queries(&self, page: u32, page_size: u32) -> Result { - let url = format!("{}/api/queries/my?page={page}&page_size={page_size}", self.base_url); - let response = self.client + let url = format!( + "{}/api/queries/my?page={page}&page_size={page_size}", + self.base_url + ); + let response = self + .client .get(&url) .send() .await @@ -47,7 +51,8 @@ impl RedashClient { pub async fn get_query(&self, query_id: u64) -> Result { let url = format!("{}/api/queries/{query_id}", self.base_url); - let response = self.client + let response = self + .client .get(&url) .send() .await @@ -63,7 +68,8 @@ impl RedashClient { pub async fn list_data_sources(&self) -> Result> { let url = format!("{}/api/data_sources", self.base_url); - let response = self.client + let response = self + .client .get(&url) .send() .await @@ -79,7 +85,8 @@ impl RedashClient { pub async fn get_data_source(&self, data_source_id: u64) -> Result { let url = format!("{}/api/data_sources/{data_source_id}", self.base_url); - let response = self.client + let response = self + .client .get(&url) .send() .await @@ -99,16 +106,22 @@ impl RedashClient { refresh: bool, ) -> Result { let url = if refresh { - format!("{}/api/data_sources/{data_source_id}/schema?refresh=true", self.base_url) + format!( + "{}/api/data_sources/{data_source_id}/schema?refresh=true", + self.base_url + ) } else { format!("{}/api/data_sources/{data_source_id}/schema", self.base_url) }; - let response = self.client + let response = self + .client .get(&url) .send() .await - .context(format!("Failed to fetch schema for data source {data_source_id}"))? + .context(format!( + "Failed to fetch schema for data source {data_source_id}" + ))? .error_for_status() .context("API returned error status")?; @@ -120,7 +133,8 @@ impl RedashClient { pub async fn create_query(&self, create_query: &CreateQuery) -> Result { let url = format!("{}/api/queries", self.base_url); - let response = self.client + let response = self + .client .post(&url) .json(create_query) .send() @@ -137,7 +151,8 @@ impl RedashClient { pub async fn create_or_update_query(&self, query: &Query) -> Result { let url = format!("{}/api/queries/{}", self.base_url, query.id); - let response = self.client + let response = self + .client .post(&url) .json(query) .send() @@ -152,14 +167,21 @@ impl RedashClient { .context("Failed to parse query update response") } - pub async fn create_visualization(&self, query_id: u64, viz: &crate::models::CreateVisualization) -> Result { + pub async fn create_visualization( + &self, + query_id: u64, + viz: &crate::models::CreateVisualization, + ) -> Result { let url = format!("{}/api/visualizations", self.base_url); - let response = self.client + let response = self + .client .post(&url) .json(viz) .send() .await - .context(format!("Failed to create visualization for query {query_id}"))? + .context(format!( + "Failed to create visualization for query {query_id}" + ))? .error_for_status() .context("API returned error status")?; @@ -169,9 +191,13 @@ impl RedashClient { .context("Failed to parse visualization create response") } - pub async fn update_visualization(&self, viz: &crate::models::Visualization) -> Result { + pub async fn update_visualization( + &self, + viz: &crate::models::Visualization, + ) -> Result { let url = format!("{}/api/visualizations/{}", self.base_url, viz.id); - let response = self.client + let response = self + .client .post(&url) .json(viz) .send() @@ -199,7 +225,11 @@ impl RedashClient { } all_queries.extend(response.results); - eprintln!("Fetched {} / {} queries...", all_queries.len(), response.count); + eprintln!( + "Fetched {} / {} queries...", + all_queries.len(), + response.count + ); #[allow(clippy::cast_possible_truncation)] if all_queries.len() >= response.count as usize { @@ -224,7 +254,8 @@ impl RedashClient { parameters, }; - let response = self.client + let response = self + .client .post(&url) .json(&request) .send() @@ -233,7 +264,10 @@ impl RedashClient { let status = response.status(); if !status.is_success() { - let error_body = response.text().await.unwrap_or_else(|_| "Unable to read error response".to_string()); + let error_body = response + .text() + .await + .unwrap_or_else(|_| "Unable to read error response".to_string()); anyhow::bail!("API returned error status {status}: {error_body}"); } @@ -248,7 +282,8 @@ impl RedashClient { pub async fn poll_job(&self, job_id: &str) -> Result { let url = format!("{}/api/jobs/{job_id}", self.base_url); - let response = self.client + let response = self + .client .get(&url) .send() .await @@ -274,11 +309,14 @@ impl RedashClient { self.base_url ); - let response = self.client + let response = self + .client .get(&url) .send() .await - .context(format!("Failed to fetch result {result_id} for query {query_id}"))? + .context(format!( + "Failed to fetch result {result_id} for query {query_id}" + ))? .error_for_status() .context("API returned error status")?; @@ -297,8 +335,8 @@ impl RedashClient { timeout_secs: u64, poll_interval_ms: u64, ) -> Result { - use tokio::time::{sleep, Duration}; use crate::models::JobStatus; + use tokio::time::{Duration, sleep}; eprintln!("Executing query {query_id}..."); let job = self.refresh_query(query_id, parameters).await?; @@ -317,14 +355,17 @@ impl RedashClient { match status { JobStatus::Success => { - let result_id = current_job.query_result_id + let result_id = current_job + .query_result_id .context("Job succeeded but no result_id returned")?; eprintln!("Query completed, fetching results..."); return self.get_query_result(query_id, result_id).await; } JobStatus::Failure => { - let error = current_job.error.unwrap_or_else(|| "Unknown error".to_string()); + let error = current_job + .error + .unwrap_or_else(|| "Unknown error".to_string()); anyhow::bail!("Query execution failed: {error}"); } JobStatus::Cancelled => { @@ -343,7 +384,8 @@ impl RedashClient { let url = format!("{}/api/queries/{query_id}", self.base_url); let payload = serde_json::json!({"is_archived": true}); - let response = self.client + let response = self + .client .post(&url) .json(&payload) .send() @@ -362,7 +404,8 @@ impl RedashClient { let url = format!("{}/api/queries/{query_id}", self.base_url); let payload = serde_json::json!({"is_archived": false}); - let response = self.client + let response = self + .client .post(&url) .json(&payload) .send() @@ -379,7 +422,8 @@ impl RedashClient { pub async fn create_dashboard(&self, dashboard: &CreateDashboard) -> Result { let url = format!("{}/api/dashboards", self.base_url); - let response = self.client + let response = self + .client .post(&url) .json(dashboard) .send() @@ -388,7 +432,11 @@ impl RedashClient { let status = response.status(); if !status.is_success() { - anyhow::bail!("HTTP {}: {}", status.as_u16(), status.canonical_reason().unwrap_or("Unknown error")); + anyhow::bail!( + "HTTP {}: {}", + status.as_u16(), + status.canonical_reason().unwrap_or("Unknown error") + ); } response @@ -397,9 +445,17 @@ impl RedashClient { .context("Failed to parse dashboard create response") } - pub async fn list_favorite_dashboards(&self, page: u32, page_size: u32) -> Result { - let url = format!("{}/api/dashboards/favorites?page={page}&page_size={page_size}", self.base_url); - let response = self.client + pub async fn list_favorite_dashboards( + &self, + page: u32, + page_size: u32, + ) -> Result { + let url = format!( + "{}/api/dashboards/favorites?page={page}&page_size={page_size}", + self.base_url + ); + let response = self + .client .get(&url) .send() .await @@ -407,7 +463,11 @@ impl RedashClient { let status = response.status(); if !status.is_success() { - anyhow::bail!("HTTP {}: {}", status.as_u16(), status.canonical_reason().unwrap_or("Unknown error")); + anyhow::bail!( + "HTTP {}: {}", + status.as_u16(), + status.canonical_reason().unwrap_or("Unknown error") + ); } response @@ -418,7 +478,8 @@ impl RedashClient { pub async fn get_dashboard(&self, slug_or_id: &str) -> Result { let url = format!("{}/api/dashboards/{slug_or_id}", self.base_url); - let response = self.client + let response = self + .client .get(&url) .send() .await @@ -427,7 +488,11 @@ impl RedashClient { let status = response.status(); if !status.is_success() { let body = response.text().await.unwrap_or_default(); - anyhow::bail!("HTTP {}: {} — {body}", status.as_u16(), status.canonical_reason().unwrap_or("Unknown error")); + anyhow::bail!( + "HTTP {}: {} — {body}", + status.as_u16(), + status.canonical_reason().unwrap_or("Unknown error") + ); } response @@ -438,7 +503,8 @@ impl RedashClient { pub async fn update_dashboard(&self, dashboard: &Dashboard) -> Result { let url = format!("{}/api/dashboards/{}", self.base_url, dashboard.id); - let response = self.client + let response = self + .client .post(&url) .json(dashboard) .send() @@ -448,7 +514,11 @@ impl RedashClient { let status = response.status(); if !status.is_success() { let body = response.text().await.unwrap_or_default(); - anyhow::bail!("HTTP {}: {} — {body}", status.as_u16(), status.canonical_reason().unwrap_or("Unknown error")); + anyhow::bail!( + "HTTP {}: {} — {body}", + status.as_u16(), + status.canonical_reason().unwrap_or("Unknown error") + ); } response @@ -460,7 +530,8 @@ impl RedashClient { pub async fn archive_dashboard(&self, dashboard_id: u64) -> Result<()> { let url = format!("{}/api/dashboards/{dashboard_id}", self.base_url); let payload = serde_json::json!({"is_archived": true}); - let response = self.client + let response = self + .client .post(&url) .json(&payload) .send() @@ -469,7 +540,11 @@ impl RedashClient { let status = response.status(); if !status.is_success() { - anyhow::bail!("HTTP {}: {}", status.as_u16(), status.canonical_reason().unwrap_or("Unknown error")); + anyhow::bail!( + "HTTP {}: {}", + status.as_u16(), + status.canonical_reason().unwrap_or("Unknown error") + ); } Ok(()) @@ -479,7 +554,8 @@ impl RedashClient { let url = format!("{}/api/dashboards/{dashboard_id}", self.base_url); let payload = serde_json::json!({"is_archived": false}); - let response = self.client + let response = self + .client .post(&url) .json(&payload) .send() @@ -488,7 +564,11 @@ impl RedashClient { let status = response.status(); if !status.is_success() { - anyhow::bail!("HTTP {}: {}", status.as_u16(), status.canonical_reason().unwrap_or("Unknown error")); + anyhow::bail!( + "HTTP {}: {}", + status.as_u16(), + status.canonical_reason().unwrap_or("Unknown error") + ); } response @@ -499,7 +579,8 @@ impl RedashClient { pub async fn create_widget(&self, widget: &CreateWidget) -> Result { let url = format!("{}/api/widgets", self.base_url); - let response = self.client + let response = self + .client .post(&url) .json(widget) .send() @@ -509,7 +590,11 @@ impl RedashClient { let status = response.status(); if !status.is_success() { let body = response.text().await.unwrap_or_default(); - anyhow::bail!("HTTP {}: {} — {body}", status.as_u16(), status.canonical_reason().unwrap_or("Unknown error")); + anyhow::bail!( + "HTTP {}: {} — {body}", + status.as_u16(), + status.canonical_reason().unwrap_or("Unknown error") + ); } response @@ -518,9 +603,14 @@ impl RedashClient { .context("Failed to parse widget create response") } - pub async fn update_widget(&self, widget_id: u64, widget: &CreateWidget) -> Result { + pub async fn update_widget( + &self, + widget_id: u64, + widget: &CreateWidget, + ) -> Result { let url = format!("{}/api/widgets/{widget_id}", self.base_url); - let response = self.client + let response = self + .client .post(&url) .json(widget) .send() @@ -530,7 +620,11 @@ impl RedashClient { let status = response.status(); if !status.is_success() { let body = response.text().await.unwrap_or_default(); - anyhow::bail!("HTTP {}: {} — {body}", status.as_u16(), status.canonical_reason().unwrap_or("Unknown error")); + anyhow::bail!( + "HTTP {}: {} — {body}", + status.as_u16(), + status.canonical_reason().unwrap_or("Unknown error") + ); } response @@ -541,7 +635,8 @@ impl RedashClient { pub async fn delete_widget(&self, widget_id: u64) -> Result<()> { let url = format!("{}/api/widgets/{widget_id}", self.base_url); - let response = self.client + let response = self + .client .delete(&url) .send() .await @@ -549,7 +644,11 @@ impl RedashClient { let status = response.status(); if !status.is_success() { - anyhow::bail!("HTTP {}: {}", status.as_u16(), status.canonical_reason().unwrap_or("Unknown error")); + anyhow::bail!( + "HTTP {}: {}", + status.as_u16(), + status.canonical_reason().unwrap_or("Unknown error") + ); } Ok(()) @@ -557,7 +656,8 @@ impl RedashClient { pub async fn favorite_dashboard(&self, slug: &str) -> Result<()> { let url = format!("{}/api/dashboards/{slug}/favorite", self.base_url); - let response = self.client + let response = self + .client .post(&url) .json(&serde_json::json!({})) .send() @@ -566,7 +666,11 @@ impl RedashClient { let status = response.status(); if !status.is_success() { - anyhow::bail!("HTTP {}: {}", status.as_u16(), status.canonical_reason().unwrap_or("Unknown error")); + anyhow::bail!( + "HTTP {}: {}", + status.as_u16(), + status.canonical_reason().unwrap_or("Unknown error") + ); } Ok(()) @@ -585,7 +689,11 @@ impl RedashClient { } all_dashboards.extend(response.results); - eprintln!("Fetched {} / {} dashboards...", all_dashboards.len(), response.count); + eprintln!( + "Fetched {} / {} dashboards...", + all_dashboards.len(), + response.count + ); #[allow(clippy::cast_possible_truncation)] if all_dashboards.len() >= response.count as usize { diff --git a/src/commands/archive.rs b/src/commands/archive.rs index bd12df0..d3040ac 100644 --- a/src/commands/archive.rs +++ b/src/commands/archive.rs @@ -40,10 +40,8 @@ fn find_query_files(query_id: u64) -> Result> { } fn delete_query_files(sql_path: &str, yaml_path: &str) -> Result<()> { - fs::remove_file(sql_path) - .context(format!("Failed to delete {sql_path}"))?; - fs::remove_file(yaml_path) - .context(format!("Failed to delete {yaml_path}"))?; + fs::remove_file(sql_path).context(format!("Failed to delete {sql_path}"))?; + fs::remove_file(yaml_path).context(format!("Failed to delete {yaml_path}"))?; Ok(()) } @@ -117,7 +115,10 @@ pub async fn cleanup(client: &RedashClient) -> Result<()> { return Ok(()); } - println!("Checking {} queries for archive status...\n", query_ids.len()); + println!( + "Checking {} queries for archive status...\n", + query_ids.len() + ); let mut cleaned_count = 0; let mut errors = Vec::new(); @@ -182,7 +183,10 @@ pub async fn unarchive(client: &RedashClient, query_ids: Vec) -> Result<()> } } - println!("\n✓ Unarchived {unarchived_count}/{} queries", query_ids.len()); + println!( + "\n✓ Unarchived {unarchived_count}/{} queries", + query_ids.len() + ); if !errors.is_empty() { anyhow::bail!("Failed to unarchive {} queries", errors.len()); diff --git a/src/commands/dashboards.rs b/src/commands/dashboards.rs index fd9f0fe..4beb840 100644 --- a/src/commands/dashboards.rs +++ b/src/commands/dashboards.rs @@ -7,8 +7,8 @@ use std::path::{Path, PathBuf}; use crate::api::RedashClient; use crate::models::{ - build_dashboard_level_parameter_mappings, CreateDashboard, CreateWidget, Dashboard, - DashboardMetadata, Query, WidgetMetadata, + CreateDashboard, CreateWidget, Dashboard, DashboardMetadata, Query, WidgetMetadata, + build_dashboard_level_parameter_mappings, }; fn extract_dashboard_slugs_from_path(dashboards_dir: &Path) -> Result> { @@ -24,7 +24,8 @@ fn extract_dashboard_slugs_from_path(dashboards_dir: &Path) -> Result Result<()> { println!("\nUsage:"); println!(" stmo-cli dashboards fetch [...]"); - println!(" stmo-cli dashboards fetch firefox-desktop-on-steamos bug-2006698---ccov-build-regression"); + println!( + " stmo-cli dashboards fetch firefox-desktop-on-steamos bug-2006698---ccov-build-regression" + ); Ok(()) } pub async fn fetch(client: &RedashClient, dashboard_slugs: Vec) -> Result<()> { if dashboard_slugs.is_empty() { - anyhow::bail!("No dashboard slugs specified. Use 'dashboards discover' to see available dashboards.\n\nExample:\n stmo-cli dashboards fetch firefox-desktop-on-steamos bug-2006698---ccov-build-regression"); + anyhow::bail!( + "No dashboard slugs specified. Use 'dashboards discover' to see available dashboards.\n\nExample:\n stmo-cli dashboards fetch firefox-desktop-on-steamos bug-2006698---ccov-build-regression" + ); } - fs::create_dir_all("dashboards") - .context("Failed to create dashboards directory")?; + fs::create_dir_all("dashboards").context("Failed to create dashboards directory")?; println!("Fetching {} dashboards...\n", dashboard_slugs.len()); @@ -134,7 +138,9 @@ pub async fn fetch(client: &RedashClient, dashboard_slugs: Vec) -> Resul if failed_slugs.is_empty() { println!("\n✓ All dashboards fetched successfully"); - println!("\nTip: Favorite these dashboards in the Redash web UI so they appear in 'dashboards discover'."); + println!( + "\nTip: Favorite these dashboards in the Redash web UI so they appear in 'dashboards discover'." + ); Ok(()) } else { println!("\n✓ {success_count} dashboard(s) fetched successfully"); @@ -153,13 +159,21 @@ pub async fn deploy(client: &RedashClient, dashboard_slugs: Vec, all: bo if existing_dashboard_slugs.is_empty() { anyhow::bail!("No dashboards found in dashboards/ directory. Use 'fetch' first."); } - println!("Deploying {} dashboards from local directory...\n", existing_dashboard_slugs.len()); + println!( + "Deploying {} dashboards from local directory...\n", + existing_dashboard_slugs.len() + ); existing_dashboard_slugs } else if !dashboard_slugs.is_empty() { - println!("Deploying {} specific dashboards...\n", dashboard_slugs.len()); + println!( + "Deploying {} specific dashboards...\n", + dashboard_slugs.len() + ); dashboard_slugs } else { - anyhow::bail!("No dashboard slugs specified. Use --all to deploy all tracked dashboards, or provide specific slugs.\n\nExamples:\n stmo-cli dashboards deploy --all\n stmo-cli dashboards deploy firefox-desktop-on-steamos bug-2006698---ccov-build-regression"); + anyhow::bail!( + "No dashboard slugs specified. Use --all to deploy all tracked dashboards, or provide specific slugs.\n\nExamples:\n stmo-cli dashboards deploy --all\n stmo-cli dashboards deploy firefox-desktop-on-steamos bug-2006698---ccov-build-regression" + ); }; let mut success_count = 0; @@ -223,16 +237,14 @@ fn save_dashboard_yaml( .collect(), }; - let yaml_content = serde_yaml::to_string(&metadata) - .context("Failed to serialize dashboard metadata")?; - fs::write(&filename, &yaml_content) - .context(format!("Failed to write {filename}"))?; + let yaml_content = + serde_yaml::to_string(&metadata).context("Failed to serialize dashboard metadata")?; + fs::write(&filename, &yaml_content).context(format!("Failed to write {filename}"))?; if let Some(old_path) = old_yaml_path && old_path != std::path::Path::new(&filename) { - fs::remove_file(&old_path) - .context(format!("Failed to delete {}", old_path.display()))?; + fs::remove_file(&old_path).context(format!("Failed to delete {}", old_path.display()))?; } Ok(()) @@ -247,7 +259,8 @@ async fn resolve_visualization_id( return Ok(Some(viz_id)); } - let (Some(query_id), Some(viz_name)) = (widget.query_id, widget.visualization_name.as_deref()) else { + let (Some(query_id), Some(viz_name)) = (widget.query_id, widget.visualization_name.as_deref()) + else { return Ok(None); }; @@ -259,7 +272,11 @@ async fn resolve_visualization_id( if let Some(viz) = query.visualizations.iter().find(|v| v.name == viz_name) { Ok(Some(viz.id)) } else { - let available: Vec<&str> = query.visualizations.iter().map(|v| v.name.as_str()).collect(); + let available: Vec<&str> = query + .visualizations + .iter() + .map(|v| v.name.as_str()) + .collect(); anyhow::bail!( "No visualization named '{viz_name}' found on query {query_id}. Available: {available:?}" ); @@ -319,24 +336,26 @@ async fn deploy_single_dashboard(client: &RedashClient, dashboard_slug: &str) -> let yaml_content = fs::read_to_string(&yaml_path) .context(format!("Failed to read {}", yaml_path.display()))?; - let local_metadata: DashboardMetadata = serde_yaml::from_str(&yaml_content) - .context("Failed to parse dashboard YAML")?; + let local_metadata: DashboardMetadata = + serde_yaml::from_str(&yaml_content).context("Failed to parse dashboard YAML")?; let (server_dashboard_id, slug_for_refetch, old_yaml_path) = if local_metadata.id == 0 { - let created = client.create_dashboard(&CreateDashboard { - name: local_metadata.name.clone(), - }).await?; - println!(" ✓ Created new dashboard: {} - {}", created.id, created.name); + let created = client + .create_dashboard(&CreateDashboard { + name: local_metadata.name.clone(), + }) + .await?; + println!( + " ✓ Created new dashboard: {} - {}", + created.id, created.name + ); client.favorite_dashboard(&created.slug).await?; (created.id, created.slug.clone(), Some(yaml_path.clone())) } else { let server_dashboard = client.get_dashboard(dashboard_slug).await?; - let server_widget_ids: std::collections::HashSet = server_dashboard - .widgets - .iter() - .map(|w| w.id) - .collect(); + let server_widget_ids: std::collections::HashSet = + server_dashboard.widgets.iter().map(|w| w.id).collect(); let local_widget_ids: std::collections::HashSet = local_metadata .widgets @@ -367,7 +386,8 @@ async fn deploy_single_dashboard(client: &RedashClient, dashboard_slug: &str) -> query_id, options.parameter_mappings.as_ref(), &mut query_cache, - ).await? + ) + .await? { options.parameter_mappings = Some(mappings); any_widget_has_params = true; @@ -375,7 +395,8 @@ async fn deploy_single_dashboard(client: &RedashClient, dashboard_slug: &str) -> let create_widget = CreateWidget { dashboard_id: server_dashboard_id, - visualization_id: resolve_visualization_id(client, widget, &mut query_cache).await?, + visualization_id: resolve_visualization_id(client, widget, &mut query_cache) + .await?, text: widget.text.clone(), width: 1, options, @@ -390,7 +411,8 @@ async fn deploy_single_dashboard(client: &RedashClient, dashboard_slug: &str) -> query_id, options.parameter_mappings.as_ref(), &mut query_cache, - ).await? + ) + .await? { options.parameter_mappings = Some(mappings); any_widget_has_params = true; @@ -398,7 +420,8 @@ async fn deploy_single_dashboard(client: &RedashClient, dashboard_slug: &str) -> let update_payload = CreateWidget { dashboard_id: server_dashboard_id, - visualization_id: resolve_visualization_id(client, widget, &mut query_cache).await?, + visualization_id: resolve_visualization_id(client, widget, &mut query_cache) + .await?, text: widget.text.clone(), width: widget.width, options, @@ -430,7 +453,9 @@ async fn deploy_single_dashboard(client: &RedashClient, dashboard_slug: &str) -> pub async fn archive(client: &RedashClient, dashboard_slugs: Vec) -> Result<()> { if dashboard_slugs.is_empty() { - anyhow::bail!("No dashboard slugs specified.\n\nExample:\n stmo-cli dashboards archive firefox-desktop-on-steamos bug-2006698---ccov-build-regression"); + anyhow::bail!( + "No dashboard slugs specified.\n\nExample:\n stmo-cli dashboards archive firefox-desktop-on-steamos bug-2006698---ccov-build-regression" + ); } println!("Archiving {} dashboards...\n", dashboard_slugs.len()); @@ -440,38 +465,36 @@ pub async fn archive(client: &RedashClient, dashboard_slugs: Vec) -> Res for slug in &dashboard_slugs { match client.get_dashboard(slug).await { - Ok(dashboard) => { - match client.archive_dashboard(dashboard.id).await { - Ok(()) => { - let yaml_files: Vec<_> = fs::read_dir("dashboards") - .context("Failed to read dashboards directory")? - .filter_map(std::result::Result::ok) - .filter(|entry| { - entry.path().extension().is_some_and(|ext| ext == "yaml") - && entry - .file_name() - .to_str() - .and_then(|name| name.strip_suffix(".yaml")) - .and_then(|name| name.split_once('-')) - .map(|(_, file_slug)| file_slug) - .is_some_and(|file_slug| file_slug == slug) - }) - .collect(); - - for file in yaml_files { - fs::remove_file(file.path()) - .context(format!("Failed to delete {}", file.path().display()))?; - } - - println!(" ✓ {} archived and local file deleted", dashboard.name); - success_count += 1; - } - Err(e) => { - eprintln!(" ⚠ Dashboard '{slug}' failed to archive: {e}"); - failed_slugs.push(slug.clone()); + Ok(dashboard) => match client.archive_dashboard(dashboard.id).await { + Ok(()) => { + let yaml_files: Vec<_> = fs::read_dir("dashboards") + .context("Failed to read dashboards directory")? + .filter_map(std::result::Result::ok) + .filter(|entry| { + entry.path().extension().is_some_and(|ext| ext == "yaml") + && entry + .file_name() + .to_str() + .and_then(|name| name.strip_suffix(".yaml")) + .and_then(|name| name.split_once('-')) + .map(|(_, file_slug)| file_slug) + .is_some_and(|file_slug| file_slug == slug) + }) + .collect(); + + for file in yaml_files { + fs::remove_file(file.path()) + .context(format!("Failed to delete {}", file.path().display()))?; } + + println!(" ✓ {} archived and local file deleted", dashboard.name); + success_count += 1; } - } + Err(e) => { + eprintln!(" ⚠ Dashboard '{slug}' failed to archive: {e}"); + failed_slugs.push(slug.clone()); + } + }, Err(e) => { eprintln!(" ⚠ Dashboard '{slug}' failed to fetch for archival: {e}"); failed_slugs.push(slug.clone()); @@ -494,7 +517,9 @@ pub async fn archive(client: &RedashClient, dashboard_slugs: Vec) -> Res pub async fn unarchive(client: &RedashClient, dashboard_slugs: Vec) -> Result<()> { if dashboard_slugs.is_empty() { - anyhow::bail!("No dashboard slugs specified.\n\nExample:\n stmo-cli dashboards unarchive firefox-desktop-on-steamos bug-2006698---ccov-build-regression"); + anyhow::bail!( + "No dashboard slugs specified.\n\nExample:\n stmo-cli dashboards unarchive firefox-desktop-on-steamos bug-2006698---ccov-build-regression" + ); } println!("Unarchiving {} dashboards...\n", dashboard_slugs.len()); @@ -504,18 +529,16 @@ pub async fn unarchive(client: &RedashClient, dashboard_slugs: Vec) -> R for slug in &dashboard_slugs { match client.get_dashboard(slug).await { - Ok(dashboard) => { - match client.unarchive_dashboard(dashboard.id).await { - Ok(unarchived) => { - println!(" ✓ {} unarchived", unarchived.name); - success_count += 1; - } - Err(e) => { - eprintln!(" ⚠ Dashboard '{slug}' failed to unarchive: {e}"); - failed_slugs.push(slug.clone()); - } + Ok(dashboard) => match client.unarchive_dashboard(dashboard.id).await { + Ok(unarchived) => { + println!(" ✓ {} unarchived", unarchived.name); + success_count += 1; } - } + Err(e) => { + eprintln!(" ⚠ Dashboard '{slug}' failed to unarchive: {e}"); + failed_slugs.push(slug.clone()); + } + }, Err(e) => { eprintln!(" ⚠ Dashboard '{slug}' failed to fetch for unarchival: {e}"); failed_slugs.push(slug.clone()); @@ -558,8 +581,16 @@ mod tests { let temp_dir = TempDir::new().unwrap(); let temp_path = temp_dir.path(); - fs::write(temp_path.join("2006698-bug-2006698---ccov-build-regression.yaml"), "test").unwrap(); - fs::write(temp_path.join("2570-firefox-desktop-on-steamos.yaml"), "test").unwrap(); + fs::write( + temp_path.join("2006698-bug-2006698---ccov-build-regression.yaml"), + "test", + ) + .unwrap(); + fs::write( + temp_path.join("2570-firefox-desktop-on-steamos.yaml"), + "test", + ) + .unwrap(); let result = extract_dashboard_slugs_from_path(temp_path); assert!(result.is_ok()); @@ -575,8 +606,16 @@ mod tests { let temp_dir = TempDir::new().unwrap(); let temp_path = temp_dir.path(); - fs::write(temp_path.join("2006698-bug-2006698---ccov-build-regression.yaml"), "test").unwrap(); - fs::write(temp_path.join("2006699-bug-2006698---ccov-build-regression.yaml"), "test").unwrap(); + fs::write( + temp_path.join("2006698-bug-2006698---ccov-build-regression.yaml"), + "test", + ) + .unwrap(); + fs::write( + temp_path.join("2006699-bug-2006698---ccov-build-regression.yaml"), + "test", + ) + .unwrap(); let result = extract_dashboard_slugs_from_path(temp_path); assert!(result.is_ok()); @@ -592,8 +631,16 @@ mod tests { let temp_dir = TempDir::new().unwrap(); let temp_path = temp_dir.path(); - fs::write(temp_path.join("2006698-bug-2006698---ccov-build-regression.yaml"), "test").unwrap(); - fs::write(temp_path.join("2570-firefox-desktop-on-steamos.txt"), "test").unwrap(); + fs::write( + temp_path.join("2006698-bug-2006698---ccov-build-regression.yaml"), + "test", + ) + .unwrap(); + fs::write( + temp_path.join("2570-firefox-desktop-on-steamos.txt"), + "test", + ) + .unwrap(); fs::write(temp_path.join("README.md"), "test").unwrap(); let result = extract_dashboard_slugs_from_path(temp_path); @@ -611,7 +658,11 @@ mod tests { let temp_path = temp_dir.path(); fs::write(temp_path.join("3000-zebra-dashboard.yaml"), "test").unwrap(); - fs::write(temp_path.join("2006698-bug-2006698---ccov-build-regression.yaml"), "test").unwrap(); + fs::write( + temp_path.join("2006698-bug-2006698---ccov-build-regression.yaml"), + "test", + ) + .unwrap(); fs::write(temp_path.join("1000-alpha-dashboard.yaml"), "test").unwrap(); let result = extract_dashboard_slugs_from_path(temp_path); diff --git a/src/commands/datasources.rs b/src/commands/datasources.rs index 409f889..bf06a77 100644 --- a/src/commands/datasources.rs +++ b/src/commands/datasources.rs @@ -1,9 +1,9 @@ #![allow(clippy::missing_errors_doc)] -use anyhow::Result; +use super::OutputFormat; use crate::api::RedashClient; use crate::models::DataSource; -use super::OutputFormat; +use anyhow::Result; fn build_status_string(ds: &DataSource) -> String { let mut status_parts = Vec::new(); @@ -63,7 +63,10 @@ pub async fn show_data_source( let ds = client.get_data_source(data_source_id).await?; let schema = if show_schema { - match client.get_data_source_schema(data_source_id, refresh_schema).await { + match client + .get_data_source_schema(data_source_id, refresh_schema) + .await + { Ok(s) => Some(s), Err(e) => { eprintln!("Error fetching schema: {e:#}"); @@ -254,10 +257,22 @@ mod tests { #[test] fn test_output_format_from_str() { - assert!(matches!("json".parse::().unwrap(), OutputFormat::Json)); - assert!(matches!("JSON".parse::().unwrap(), OutputFormat::Json)); - assert!(matches!("table".parse::().unwrap(), OutputFormat::Table)); - assert!(matches!("TABLE".parse::().unwrap(), OutputFormat::Table)); + assert!(matches!( + "json".parse::().unwrap(), + OutputFormat::Json + )); + assert!(matches!( + "JSON".parse::().unwrap(), + OutputFormat::Json + )); + assert!(matches!( + "table".parse::().unwrap(), + OutputFormat::Table + )); + assert!(matches!( + "TABLE".parse::().unwrap(), + OutputFormat::Table + )); assert!("invalid".parse::().is_err()); assert!("csv".parse::().is_err()); } diff --git a/src/commands/deploy.rs b/src/commands/deploy.rs index 7e21d67..e120e62 100644 --- a/src/commands/deploy.rs +++ b/src/commands/deploy.rs @@ -1,12 +1,12 @@ #![allow(clippy::missing_errors_doc)] -use anyhow::{bail, Context, Result}; +use crate::api::RedashClient; +use crate::models::Query; +use anyhow::{Context, Result, bail}; +use std::collections::HashSet; use std::fs; use std::path::Path; use std::process::Command; -use std::collections::HashSet; -use crate::api::RedashClient; -use crate::models::Query; fn slugify(s: &str) -> String { s.to_lowercase() @@ -86,8 +86,8 @@ fn get_all_query_metadata() -> Result> { let path = entry.path(); if path.extension().is_some_and(|ext| ext == "yaml") { - let metadata_content = fs::read_to_string(&path) - .context(format!("Failed to read {}", path.display()))?; + let metadata_content = + fs::read_to_string(&path).context(format!("Failed to read {}", path.display()))?; let metadata: crate::models::QueryMetadata = serde_yaml::from_str(&metadata_content) .context(format!("Failed to parse {}", path.display()))?; @@ -134,7 +134,10 @@ async fn deploy_visualizations( description: viz.description.clone(), }; client.update_visualization(&viz_to_update).await?; - println!(" ✓ Updated visualization: {} (ID: {})", viz_to_update.name, server_viz.id); + println!( + " ✓ Updated visualization: {} (ID: {})", + viz_to_update.name, server_viz.id + ); } else { let viz_to_create = crate::models::CreateVisualization { query_id, @@ -143,8 +146,13 @@ async fn deploy_visualizations( options: viz.options.clone(), description: viz.description.clone(), }; - let created = client.create_visualization(query_id, &viz_to_create).await?; - println!(" ✓ Created visualization: {} (ID: {})", created.name, created.id); + let created = client + .create_visualization(query_id, &viz_to_create) + .await?; + println!( + " ✓ Created visualization: {} (ID: {})", + created.name, created.id + ); } } } @@ -215,11 +223,10 @@ pub async fn deploy(client: &RedashClient, query_ids: Vec, all: bool) -> Re bail!("Query metadata file not found: {yaml_path}"); } - let sql = fs::read_to_string(&sql_path) - .context(format!("Failed to read {sql_path}"))?; + let sql = fs::read_to_string(&sql_path).context(format!("Failed to read {sql_path}"))?; - let metadata_content = fs::read_to_string(&yaml_path) - .context(format!("Failed to read {yaml_path}"))?; + let metadata_content = + fs::read_to_string(&yaml_path).context(format!("Failed to read {yaml_path}"))?; let metadata: crate::models::QueryMetadata = serde_yaml::from_str(&metadata_content) .context(format!("Failed to parse {yaml_path}"))?; @@ -265,10 +272,8 @@ pub async fn deploy(client: &RedashClient, query_ids: Vec, all: bool) -> Re .context("Failed to serialize query metadata")?; fs::write(format!("{new_base}.yaml"), yaml_content) .context(format!("Failed to write {new_base}.yaml"))?; - fs::remove_file(&sql_path) - .context(format!("Failed to delete {sql_path}"))?; - fs::remove_file(&yaml_path) - .context(format!("Failed to delete {yaml_path}"))?; + fs::remove_file(&sql_path).context(format!("Failed to delete {sql_path}"))?; + fs::remove_file(&yaml_path).context(format!("Failed to delete {yaml_path}"))?; println!(" ✓ Created new query: {} - {name}", fetched.id); println!(" Renamed: 0-{slug}.* → {}-{new_slug}.*", fetched.id); fetched @@ -310,13 +315,18 @@ pub async fn deploy(client: &RedashClient, query_ids: Vec, all: bool) -> Re }; let yaml_content = serde_yaml::to_string(&updated_metadata) .context("Failed to serialize query metadata")?; - fs::write(&yaml_path, yaml_content) - .context(format!("Failed to write {yaml_path}"))?; + fs::write(&yaml_path, yaml_content).context(format!("Failed to write {yaml_path}"))?; println!(" ✓ {id} - {name}"); result }; - deploy_visualizations(client, result_query.id, &metadata.visualizations, &result_query.visualizations).await?; + deploy_visualizations( + client, + result_query.id, + &metadata.visualizations, + &result_query.visualizations, + ) + .await?; } println!("\n✓ All resources deployed successfully"); diff --git a/src/commands/discover.rs b/src/commands/discover.rs index bee0be1..187556f 100644 --- a/src/commands/discover.rs +++ b/src/commands/discover.rs @@ -1,7 +1,7 @@ #![allow(clippy::missing_errors_doc)] -use anyhow::Result; use crate::api::RedashClient; +use anyhow::Result; pub async fn discover(client: &RedashClient) -> Result<()> { println!("Fetching your queries from Redash...\n"); diff --git a/src/commands/execute.rs b/src/commands/execute.rs index 64413c7..ee58ffc 100644 --- a/src/commands/execute.rs +++ b/src/commands/execute.rs @@ -1,13 +1,13 @@ #![allow(clippy::missing_errors_doc)] #![allow(clippy::missing_panics_doc)] +use super::OutputFormat; +use crate::api::RedashClient; +use crate::models::{Parameter, QueryMetadata}; use anyhow::{Context, Result, bail}; use std::collections::HashMap; use std::fs; use std::path::Path; -use crate::api::RedashClient; -use crate::models::{QueryMetadata, Parameter}; -use super::OutputFormat; fn parse_parameter_arg(arg: &str) -> Result<(String, serde_json::Value)> { let parts: Vec<&str> = arg.splitn(2, '=').collect(); @@ -40,8 +40,8 @@ fn load_query_metadata_by_id(query_id: u64) -> Result<(QueryMetadata, String, St && let Ok(id) = id_str.parse::() && id == query_id { - let yaml_content = fs::read_to_string(&path) - .context(format!("Failed to read {}", path.display()))?; + let yaml_content = + fs::read_to_string(&path).context(format!("Failed to read {}", path.display()))?; let metadata: QueryMetadata = serde_yaml::from_str(&yaml_content) .context(format!("Failed to parse {}", path.display()))?; @@ -53,14 +53,16 @@ fn load_query_metadata_by_id(query_id: u64) -> Result<(QueryMetadata, String, St bail!("SQL file not found: {sql_path}"); } - let sql = fs::read_to_string(&sql_path) - .context(format!("Failed to read {sql_path}"))?; + let sql = + fs::read_to_string(&sql_path).context(format!("Failed to read {sql_path}"))?; return Ok((metadata, sql, yaml_path)); } } - bail!("Query {query_id} not found in queries/ directory. Run 'stmo-cli fetch {query_id}' first."); + bail!( + "Query {query_id} not found in queries/ directory. Run 'stmo-cli fetch {query_id}' first." + ); } fn prompt_for_parameter(param: &Parameter) -> Result { @@ -86,12 +88,14 @@ fn prompt_for_parameter(param: &Parameter) -> Result { .items(&options) .interact()?; - let selected: Vec = selections.iter() - .map(|&i| options[i].to_string()) - .collect(); + let selected: Vec = + selections.iter().map(|&i| options[i].to_string()).collect(); Ok(serde_json::Value::Array( - selected.into_iter().map(serde_json::Value::String).collect() + selected + .into_iter() + .map(serde_json::Value::String) + .collect(), )) } else { let selection = Select::new() @@ -103,22 +107,16 @@ fn prompt_for_parameter(param: &Parameter) -> Result { Ok(serde_json::Value::String(options[selection].to_string())) } } else { - let input: String = Input::new() - .with_prompt(title) - .interact_text()?; + let input: String = Input::new().with_prompt(title).interact_text()?; Ok(serde_json::Value::String(input)) } } "number" => { - let input: f64 = Input::new() - .with_prompt(title) - .interact_text()?; + let input: f64 = Input::new().with_prompt(title).interact_text()?; Ok(serde_json::json!(input)) } _ => { - let input: String = Input::new() - .with_prompt(title) - .interact_text()?; + let input: String = Input::new().with_prompt(title).interact_text()?; Ok(serde_json::Value::String(input)) } } @@ -150,24 +148,38 @@ fn build_parameter_map( } else { bail!( "Missing required parameter: '{}' ({}). Use --param {}=value or --interactive", - param.name, param.title, param.name + param.name, + param.title, + param.name ); } } } - Ok(if param_map.is_empty() { None } else { Some(param_map) }) + Ok(if param_map.is_empty() { + None + } else { + Some(param_map) + }) } -fn format_results_json(result: &crate::models::QueryResult, limit: Option) -> Result { +fn format_results_json( + result: &crate::models::QueryResult, + limit: Option, +) -> Result { let rows = if let Some(limit) = limit { - result.data.rows.iter().take(limit).cloned().collect::>() + result + .data + .rows + .iter() + .take(limit) + .cloned() + .collect::>() } else { result.data.rows.clone() }; - serde_json::to_string_pretty(&rows) - .context("Failed to format results as JSON") + serde_json::to_string_pretty(&rows).context("Failed to format results as JSON") } fn format_results_table(result: &crate::models::QueryResult, limit: Option) -> String { @@ -182,12 +194,15 @@ fn format_results_table(result: &crate::models::QueryResult, limit: Option "NULL".to_string(), serde_json::Value::String(s) => s.clone(), @@ -208,15 +223,21 @@ fn format_results_table(result: &crate::models::QueryResult, limit: Option().unwrap(), OutputFormat::Json)); - assert!(matches!("JSON".parse::().unwrap(), OutputFormat::Json)); - assert!(matches!("table".parse::().unwrap(), OutputFormat::Table)); - assert!(matches!("TABLE".parse::().unwrap(), OutputFormat::Table)); + assert!(matches!( + "json".parse::().unwrap(), + OutputFormat::Json + )); + assert!(matches!( + "JSON".parse::().unwrap(), + OutputFormat::Json + )); + assert!(matches!( + "table".parse::().unwrap(), + OutputFormat::Table + )); + assert!(matches!( + "TABLE".parse::().unwrap(), + OutputFormat::Table + )); } #[test] diff --git a/src/commands/fetch.rs b/src/commands/fetch.rs index 61bb382..ac3144d 100644 --- a/src/commands/fetch.rs +++ b/src/commands/fetch.rs @@ -46,16 +46,20 @@ fn extract_query_ids_from_directory() -> Result> { } pub async fn fetch(client: &RedashClient, query_ids: Vec, all: bool) -> Result<()> { - fs::create_dir_all("queries") - .context("Failed to create queries directory")?; + fs::create_dir_all("queries").context("Failed to create queries directory")?; let existing_query_ids = extract_query_ids_from_directory()?; let queries_to_fetch = if all { if existing_query_ids.is_empty() { - anyhow::bail!("No queries found in queries/ directory. Use specific query IDs or run 'discover' to see available queries."); + anyhow::bail!( + "No queries found in queries/ directory. Use specific query IDs or run 'discover' to see available queries." + ); } - println!("Fetching {} queries from local directory...\n", existing_query_ids.len()); + println!( + "Fetching {} queries from local directory...\n", + existing_query_ids.len() + ); let mut queries = Vec::new(); for id in &existing_query_ids { match client.get_query(*id).await { @@ -75,7 +79,9 @@ pub async fn fetch(client: &RedashClient, query_ids: Vec, all: bool) -> Res } queries } else { - anyhow::bail!("No query IDs specified. Use --all to fetch tracked queries, or provide specific query IDs.\n\nExamples:\n stmo-cli fetch --all\n stmo-cli fetch 123 456 789\n stmo-cli discover (to see available queries)"); + anyhow::bail!( + "No query IDs specified. Use --all to fetch tracked queries, or provide specific query IDs.\n\nExamples:\n stmo-cli fetch --all\n stmo-cli fetch 123 456 789\n stmo-cli discover (to see available queries)" + ); }; println!("Fetching {} queries...", queries_to_fetch.len()); @@ -87,10 +93,10 @@ pub async fn fetch(client: &RedashClient, query_ids: Vec, all: bool) -> Res let filename_base = format!("{}-{}", query.id, slug); let sql_path = format!("queries/{filename_base}.sql"); - fs::write(&sql_path, &query.sql) - .context(format!("Failed to write {sql_path}"))?; + fs::write(&sql_path, &query.sql).context(format!("Failed to write {sql_path}"))?; - let mut visualizations: Vec = query.visualizations + let mut visualizations: Vec = query + .visualizations .iter() .map(crate::models::VisualizationMetadata::from) .collect(); @@ -108,10 +114,9 @@ pub async fn fetch(client: &RedashClient, query_ids: Vec, all: bool) -> Res }; let yaml_path = format!("queries/{filename_base}.yaml"); - let yaml_content = serde_yaml::to_string(&metadata) - .context("Failed to serialize query metadata")?; - fs::write(&yaml_path, yaml_content) - .context(format!("Failed to write {yaml_path}"))?; + let yaml_content = + serde_yaml::to_string(&metadata).context("Failed to serialize query metadata")?; + fs::write(&yaml_path, yaml_content).context(format!("Failed to write {yaml_path}"))?; if query.is_archived { archived_queries.push((query.id, query.name.clone())); @@ -124,12 +129,20 @@ pub async fn fetch(client: &RedashClient, query_ids: Vec, all: bool) -> Res println!("\n✓ All resources fetched successfully"); if !archived_queries.is_empty() { - println!("\n⚠ Warning: {} archived queries have local files:", archived_queries.len()); + println!( + "\n⚠ Warning: {} archived queries have local files:", + archived_queries.len() + ); for (id, name) in &archived_queries { println!(" - {id}: {name}"); } - let binary_name = std::env::args().next() - .and_then(|path| std::path::Path::new(&path).file_name().map(|s| s.to_string_lossy().to_string())) + let binary_name = std::env::args() + .next() + .and_then(|path| { + std::path::Path::new(&path) + .file_name() + .map(|s| s.to_string_lossy().to_string()) + }) .unwrap_or_else(|| "stmo-cli".to_string()); println!("\nConsider cleaning up with: {binary_name} archive --cleanup"); } @@ -180,6 +193,9 @@ mod tests { #[test] fn test_slugify_mixed() { assert_eq!(slugify("Mozilla's .deb Package!"), "mozilla-s-deb-package"); - assert_eq!(slugify("Copy of 100234 - Gecko decision task"), "copy-of-100234-gecko-decision-task"); + assert_eq!( + slugify("Copy of 100234 - Gecko decision task"), + "copy-of-100234-gecko-decision-task" + ); } } diff --git a/src/commands/init.rs b/src/commands/init.rs index 3017a11..7abb593 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -54,8 +54,7 @@ fn write_if_missing(target_dir: &Path, file: &ScaffoldFile) -> Result { Ok(false) } else { let path = file.path; - fs::write(&file_path, file.content) - .with_context(|| format!("Failed to write {path}"))?; + fs::write(&file_path, file.content).with_context(|| format!("Failed to write {path}"))?; let description = file.description; println!(" ✓ {path} ({description})"); Ok(true) @@ -165,7 +164,11 @@ fn setup_git_repo(target_dir: &Path, files_created: bool) -> Result<()> { } let commit_output = Command::new("git") - .args(["commit", "-m", "Initial commit: scaffold query/dashboard repository"]) + .args([ + "commit", + "-m", + "Initial commit: scaffold query/dashboard repository", + ]) .current_dir(target_dir) .output() .context("Failed to run git commit")?; @@ -299,13 +302,25 @@ pub fn init() -> Result<()> { #[cfg(test)] mod tests { use super::*; - use tempfile::TempDir; use std::fs; + use tempfile::TempDir; fn setup_test_repo(dir: &std::path::Path) { - Command::new("git").arg("init").current_dir(dir).status().unwrap(); - Command::new("git").args(["config", "user.name", "Test"]).current_dir(dir).status().unwrap(); - Command::new("git").args(["config", "user.email", "test@test"]).current_dir(dir).status().unwrap(); + Command::new("git") + .arg("init") + .current_dir(dir) + .status() + .unwrap(); + Command::new("git") + .args(["config", "user.name", "Test"]) + .current_dir(dir) + .status() + .unwrap(); + Command::new("git") + .args(["config", "user.email", "test@test"]) + .current_dir(dir) + .status() + .unwrap(); } #[test] @@ -321,7 +336,8 @@ mod tests { assert!(temp_dir.path().join("queries/.gitkeep").exists()); assert!(temp_dir.path().join("dashboards/.gitkeep").exists()); - let pre_commit_content = fs::read_to_string(temp_dir.path().join(".pre-commit-config.yaml")).unwrap(); + let pre_commit_content = + fs::read_to_string(temp_dir.path().join(".pre-commit-config.yaml")).unwrap(); assert!(pre_commit_content.contains("yamllint")); assert!(pre_commit_content.contains("sqlfluff")); diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 194f8a3..36c07f4 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,14 +1,14 @@ -pub mod discover; -pub mod init; -pub mod fetch; -pub mod deploy; -pub mod execute; -pub mod datasources; pub mod archive; pub mod dashboards; +pub mod datasources; +pub mod deploy; +pub mod discover; +pub mod execute; +pub mod fetch; +pub mod init; pub mod update; -use anyhow::{bail, Result}; +use anyhow::{Result, bail}; #[derive(Debug, Clone, Copy)] pub enum OutputFormat { diff --git a/src/main.rs b/src/main.rs index c7bcea9..6c9309a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,8 +3,8 @@ mod commands; mod models; use anyhow::{Context, Result}; -use clap::{Parser, Subcommand}; use api::RedashClient; +use clap::{Parser, Subcommand}; use moz_cli_version_check::VersionChecker; #[derive(Parser)] @@ -27,7 +27,10 @@ enum Commands { Fetch { #[arg(help = "Query IDs to fetch (e.g., 123 456 789)")] query_ids: Vec, - #[arg(long, help = "Fetch all queries currently tracked in queries/ directory")] + #[arg( + long, + help = "Fetch all queries currently tracked in queries/ directory" + )] all: bool, }, @@ -44,13 +47,25 @@ enum Commands { #[arg(help = "Query ID to execute (must be fetched locally first)")] query_id: u64, - #[arg(long, help = "Query parameter in format: name=value (can be used multiple times)")] + #[arg( + long, + help = "Query parameter in format: name=value (can be used multiple times)" + )] param: Vec, - #[arg(long, short = 'f', default_value = "json", help = "Output format: json or table")] + #[arg( + long, + short = 'f', + default_value = "json", + help = "Output format: json or table" + )] format: String, - #[arg(long, short = 'i', help = "Prompt for missing parameters interactively")] + #[arg( + long, + short = 'i', + help = "Prompt for missing parameters interactively" + )] interactive: bool, #[arg(long, default_value = "300", help = "Timeout in seconds")] @@ -68,10 +83,18 @@ enum Commands { #[arg(long, help = "Show table schema for the data source")] schema: bool, - #[arg(long, help = "Force refresh schema from data source (slower but always up-to-date)")] + #[arg( + long, + help = "Force refresh schema from data source (slower but always up-to-date)" + )] refresh: bool, - #[arg(long, short = 'f', default_value = "json", help = "Output format: json or table")] + #[arg( + long, + short = 'f', + default_value = "json", + help = "Output format: json or table" + )] format: String, }, @@ -80,7 +103,10 @@ enum Commands { #[arg(help = "Query IDs to archive (e.g., 123 456 789)")] query_ids: Vec, - #[arg(long, help = "Remove local files for queries already archived in Redash")] + #[arg( + long, + help = "Remove local files for queries already archived in Redash" + )] cleanup: bool, }, @@ -107,13 +133,17 @@ enum DashboardCommands { #[command(about = "Fetch dashboards from Redash")] Fetch { - #[arg(help = "Dashboard slugs to fetch (e.g., firefox-desktop-on-steamos bug-2006698---ccov-build-regression)")] + #[arg( + help = "Dashboard slugs to fetch (e.g., firefox-desktop-on-steamos bug-2006698---ccov-build-regression)" + )] slugs: Vec, }, #[command(about = "Deploy dashboard changes to Redash")] Deploy { - #[arg(help = "Dashboard slugs to deploy (e.g., firefox-desktop-on-steamos bug-2006698---ccov-build-regression)")] + #[arg( + help = "Dashboard slugs to deploy (e.g., firefox-desktop-on-steamos bug-2006698---ccov-build-regression)" + )] slugs: Vec, #[arg(long, help = "Deploy all tracked dashboards")] all: bool, @@ -121,13 +151,17 @@ enum DashboardCommands { #[command(about = "Archive dashboards in Redash and remove local files")] Archive { - #[arg(help = "Dashboard slugs to archive (e.g., firefox-desktop-on-steamos bug-2006698---ccov-build-regression)")] + #[arg( + help = "Dashboard slugs to archive (e.g., firefox-desktop-on-steamos bug-2006698---ccov-build-regression)" + )] slugs: Vec, }, #[command(about = "Restore archived dashboards")] Unarchive { - #[arg(help = "Dashboard slugs to unarchive (e.g., firefox-desktop-on-steamos bug-2006698---ccov-build-regression)")] + #[arg( + help = "Dashboard slugs to unarchive (e.g., firefox-desktop-on-steamos bug-2006698---ccov-build-regression)" + )] slugs: Vec, }, } @@ -162,8 +196,8 @@ async fn main() -> Result<()> { return result; } - let api_key = std::env::var("REDASH_API_KEY") - .context("REDASH_API_KEY environment variable not set")?; + let api_key = + std::env::var("REDASH_API_KEY").context("REDASH_API_KEY environment variable not set")?; let base_url = std::env::var("REDASH_URL") .unwrap_or_else(|_| "https://sql.telemetry.mozilla.org".to_string()); @@ -173,10 +207,22 @@ async fn main() -> Result<()> { match cli.command { Commands::Discover => commands::discover::discover(&client).await?, Commands::Init => unreachable!("Init handled above"), - Commands::Fetch { query_ids, all } => commands::fetch::fetch(&client, query_ids, all).await?, - Commands::Deploy { query_ids, all } => commands::deploy::deploy(&client, query_ids, all).await?, - Commands::Execute { query_id, param, format, interactive, timeout, limit } => { - let output_format = format.parse::() + Commands::Fetch { query_ids, all } => { + commands::fetch::fetch(&client, query_ids, all).await? + } + Commands::Deploy { query_ids, all } => { + commands::deploy::deploy(&client, query_ids, all).await? + } + Commands::Execute { + query_id, + param, + format, + interactive, + timeout, + limit, + } => { + let output_format = format + .parse::() .context("Invalid output format")?; let limit_rows = limit; commands::execute::execute( @@ -187,14 +233,28 @@ async fn main() -> Result<()> { interactive, timeout, limit_rows, - ).await?; + ) + .await?; } - Commands::DataSources { data_source_id, schema, refresh, format } => { - let output_format = format.parse::() + Commands::DataSources { + data_source_id, + schema, + refresh, + format, + } => { + let output_format = format + .parse::() .context("Invalid output format")?; if let Some(id) = data_source_id { - commands::datasources::show_data_source(&client, id, schema, refresh, output_format).await?; + commands::datasources::show_data_source( + &client, + id, + schema, + refresh, + output_format, + ) + .await?; } else { commands::datasources::list_data_sources(&client, output_format).await?; } @@ -205,21 +265,33 @@ async fn main() -> Result<()> { } else if !query_ids.is_empty() { commands::archive::archive(&client, query_ids).await?; } else { - anyhow::bail!("No query IDs specified. Use specific query IDs or --cleanup flag.\n\nExamples:\n stmo-cli archive 123 456\n stmo-cli archive --cleanup"); + anyhow::bail!( + "No query IDs specified. Use specific query IDs or --cleanup flag.\n\nExamples:\n stmo-cli archive 123 456\n stmo-cli archive --cleanup" + ); } } Commands::Unarchive { query_ids } => { if query_ids.is_empty() { - anyhow::bail!("No query IDs specified. Provide query IDs to unarchive.\n\nExample:\n stmo-cli unarchive 123 456"); + anyhow::bail!( + "No query IDs specified. Provide query IDs to unarchive.\n\nExample:\n stmo-cli unarchive 123 456" + ); } commands::archive::unarchive(&client, query_ids).await?; } Commands::Dashboards { command } => match command { DashboardCommands::Discover => commands::dashboards::discover(&client).await?, - DashboardCommands::Fetch { slugs } => commands::dashboards::fetch(&client, slugs.clone()).await?, - DashboardCommands::Deploy { slugs, all } => commands::dashboards::deploy(&client, slugs.clone(), all).await?, - DashboardCommands::Archive { slugs } => commands::dashboards::archive(&client, slugs.clone()).await?, - DashboardCommands::Unarchive { slugs } => commands::dashboards::unarchive(&client, slugs.clone()).await?, + DashboardCommands::Fetch { slugs } => { + commands::dashboards::fetch(&client, slugs.clone()).await? + } + DashboardCommands::Deploy { slugs, all } => { + commands::dashboards::deploy(&client, slugs.clone(), all).await? + } + DashboardCommands::Archive { slugs } => { + commands::dashboards::archive(&client, slugs.clone()).await? + } + DashboardCommands::Unarchive { slugs } => { + commands::dashboards::unarchive(&client, slugs.clone()).await? + } }, Commands::Update => unreachable!("Update handled above"), } diff --git a/src/models.rs b/src/models.rs index 773d0ff..5c9922b 100644 --- a/src/models.rs +++ b/src/models.rs @@ -148,7 +148,11 @@ pub struct QueriesResponse { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct VisualizationMetadata { - #[serde(default, deserialize_with = "deserialize_viz_id", skip_serializing_if = "Option::is_none")] + #[serde( + default, + deserialize_with = "deserialize_viz_id", + skip_serializing_if = "Option::is_none" + )] pub id: Option, pub name: String, #[serde(rename = "type")] @@ -354,7 +358,11 @@ pub struct VisualizationQuery { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct WidgetOptions { pub position: WidgetPosition, - #[serde(default, skip_serializing_if = "Option::is_none", rename = "parameterMappings")] + #[serde( + default, + skip_serializing_if = "Option::is_none", + rename = "parameterMappings" + )] pub parameter_mappings: Option, } @@ -428,13 +436,16 @@ pub struct CreateWidget { pub fn build_dashboard_level_parameter_mappings(parameters: &[Parameter]) -> serde_json::Value { let mut mappings = serde_json::Map::new(); for param in parameters { - mappings.insert(param.name.clone(), serde_json::json!({ - "mapTo": param.name, - "name": param.name, - "title": "", - "type": "dashboard-level", - "value": null, - })); + mappings.insert( + param.name.clone(), + serde_json::json!({ + "mapTo": param.name, + "name": param.name, + "title": "", + "type": "dashboard-level", + "value": null, + }), + ); } serde_json::Value::Object(mappings) } @@ -451,7 +462,10 @@ mod tests { assert!(matches!(JobStatus::from_u8(2).unwrap(), JobStatus::Started)); assert!(matches!(JobStatus::from_u8(3).unwrap(), JobStatus::Success)); assert!(matches!(JobStatus::from_u8(4).unwrap(), JobStatus::Failure)); - assert!(matches!(JobStatus::from_u8(5).unwrap(), JobStatus::Cancelled)); + assert!(matches!( + JobStatus::from_u8(5).unwrap(), + JobStatus::Cancelled + )); } #[test] diff --git a/tests/api_integration.rs b/tests/api_integration.rs index 04c1215..3170102 100644 --- a/tests/api_integration.rs +++ b/tests/api_integration.rs @@ -3,10 +3,10 @@ mod common; +use common::*; use stmo_cli::api::RedashClient; -use wiremock::{MockServer, Mock, ResponseTemplate}; use wiremock::matchers::{method, path, query_param}; -use common::*; +use wiremock::{Mock, MockServer, ResponseTemplate}; #[tokio::test] async fn test_refresh_query_success() { @@ -30,23 +30,24 @@ async fn test_refresh_query_with_parameters() { Mock::given(method("POST")) .and(path("/api/queries/123/results")) - .respond_with(ResponseTemplate::new(200).set_body_json( - serde_json::json!({ - "job": { - "id": "test-job-id", - "status": 1, - "query_result_id": null, - "error": null - } - }) - )) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "job": { + "id": "test-job-id", + "status": 1, + "query_result_id": null, + "error": null + } + }))) .mount(&mock_server) .await; let client = RedashClient::new(mock_server.uri(), "test-key").unwrap(); let mut params = std::collections::HashMap::new(); params.insert("start_date".to_string(), serde_json::json!("2025-01-01")); - params.insert("channels".to_string(), serde_json::json!(["release", "beta"])); + params.insert( + "channels".to_string(), + serde_json::json!(["release", "beta"]), + ); let job = client.refresh_query(123, Some(params)).await.unwrap(); @@ -106,9 +107,7 @@ async fn test_poll_job_failure() { async fn test_get_query_result_success() { let mock_server = MockServer::start().await; - mock_get_query_result(123, 456) - .mount(&mock_server) - .await; + mock_get_query_result(123, 456).mount(&mock_server).await; let client = RedashClient::new(mock_server.uri(), "test-key").unwrap(); let result = client.get_query_result(123, 456).await.unwrap(); @@ -140,7 +139,10 @@ async fn test_execute_query_with_polling_success() { .await; let client = RedashClient::new(mock_server.uri(), "test-key").unwrap(); - let result = client.execute_query_with_polling(123, None, 10, 100).await.unwrap(); + let result = client + .execute_query_with_polling(123, None, 10, 100) + .await + .unwrap(); assert_eq!(result.id, 456); assert_eq!(result.data.columns.len(), 2); @@ -196,14 +198,12 @@ async fn test_list_my_queries_pagination() { .and(path("/api/queries/my")) .and(query_param("page", "1")) .and(query_param("page_size", "100")) - .respond_with(ResponseTemplate::new(200).set_body_json( - serde_json::json!({ - "results": [], - "count": 0, - "page": 1, - "page_size": 100 - }) - )) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "results": [], + "count": 0, + "page": 1, + "page_size": 100 + }))) .mount(&mock_server) .await; @@ -219,9 +219,7 @@ async fn test_list_my_queries_pagination() { async fn test_list_data_sources_success() { let mock_server = MockServer::start().await; - mock_list_data_sources() - .mount(&mock_server) - .await; + mock_list_data_sources().mount(&mock_server).await; let client = RedashClient::new(mock_server.uri(), "test-key").unwrap(); let data_sources = client.list_data_sources().await.unwrap(); @@ -239,9 +237,7 @@ async fn test_list_data_sources_success() { async fn test_list_data_sources_empty() { let mock_server = MockServer::start().await; - mock_list_data_sources_empty() - .mount(&mock_server) - .await; + mock_list_data_sources_empty().mount(&mock_server).await; let client = RedashClient::new(mock_server.uri(), "test-key").unwrap(); let data_sources = client.list_data_sources().await.unwrap(); @@ -253,9 +249,7 @@ async fn test_list_data_sources_empty() { async fn test_get_data_source_success() { let mock_server = MockServer::start().await; - mock_get_data_source(63) - .mount(&mock_server) - .await; + mock_get_data_source(63).mount(&mock_server).await; let client = RedashClient::new(mock_server.uri(), "test-key").unwrap(); let data_source = client.get_data_source(63).await.unwrap(); @@ -263,7 +257,10 @@ async fn test_get_data_source_success() { assert_eq!(data_source.id, 63); assert_eq!(data_source.name, "Test Data Source"); assert_eq!(data_source.ds_type, "bigquery"); - assert_eq!(data_source.description, Some("Test description".to_string())); + assert_eq!( + data_source.description, + Some("Test description".to_string()) + ); } #[tokio::test] @@ -284,9 +281,7 @@ async fn test_get_data_source_not_found() { async fn test_get_data_source_schema_success() { let mock_server = MockServer::start().await; - mock_get_data_source_schema(63) - .mount(&mock_server) - .await; + mock_get_data_source_schema(63).mount(&mock_server).await; let client = RedashClient::new(mock_server.uri(), "test-key").unwrap(); let schema = client.get_data_source_schema(63, false).await.unwrap(); @@ -336,9 +331,7 @@ async fn test_archive_query_success() { async fn test_archive_query_not_found() { let mock_server = MockServer::start().await; - mock_archive_query_not_found(999) - .mount(&mock_server) - .await; + mock_archive_query_not_found(999).mount(&mock_server).await; let client = RedashClient::new(mock_server.uri(), "test-key").unwrap(); let result = client.archive_query(999).await; @@ -412,9 +405,7 @@ async fn test_get_query_not_archived() { async fn test_list_favorite_dashboards_success() { let mock_server = MockServer::start().await; - mock_list_favorite_dashboards(2) - .mount(&mock_server) - .await; + mock_list_favorite_dashboards(2).mount(&mock_server).await; let client = RedashClient::new(mock_server.uri(), "test-key").unwrap(); let response = client.list_favorite_dashboards(1, 100).await.unwrap(); @@ -521,9 +512,7 @@ async fn test_update_dashboard_success() { async fn test_archive_dashboard_success() { let mock_server = MockServer::start().await; - mock_archive_dashboard(2570) - .mount(&mock_server) - .await; + mock_archive_dashboard(2570).mount(&mock_server).await; let client = RedashClient::new(mock_server.uri(), "test-key").unwrap(); let result = client.archive_dashboard(2570).await; @@ -579,9 +568,7 @@ async fn test_unarchive_dashboard_forbidden() { async fn test_create_widget_success() { let mock_server = MockServer::start().await; - mock_create_widget(2570, 75035) - .mount(&mock_server) - .await; + mock_create_widget(2570, 75035).mount(&mock_server).await; let client = RedashClient::new(mock_server.uri(), "test-key").unwrap(); @@ -611,9 +598,7 @@ async fn test_create_widget_success() { async fn test_delete_widget_success() { let mock_server = MockServer::start().await; - mock_delete_widget(75035) - .mount(&mock_server) - .await; + mock_delete_widget(75035).mount(&mock_server).await; let client = RedashClient::new(mock_server.uri(), "test-key").unwrap(); let result = client.delete_widget(75035).await; @@ -625,9 +610,7 @@ async fn test_delete_widget_success() { async fn test_delete_widget_not_found() { let mock_server = MockServer::start().await; - mock_delete_widget_not_found(999) - .mount(&mock_server) - .await; + mock_delete_widget_not_found(999).mount(&mock_server).await; let client = RedashClient::new(mock_server.uri(), "test-key").unwrap(); let result = client.delete_widget(999).await; @@ -641,10 +624,10 @@ async fn test_refresh_query_bad_request_includes_error_body() { mock_refresh_query_bad_request( 123, - "The following parameter values are incompatible with their definitions: worker_pool" + "The following parameter values are incompatible with their definitions: worker_pool", ) - .mount(&mock_server) - .await; + .mount(&mock_server) + .await; let client = RedashClient::new(mock_server.uri(), "test-key").unwrap(); let result = client.refresh_query(123, None).await; @@ -652,16 +635,17 @@ async fn test_refresh_query_bad_request_includes_error_body() { assert!(result.is_err()); let err = result.unwrap_err(); assert!(err.to_string().contains("400")); - assert!(err.to_string().contains("parameter values are incompatible")); + assert!( + err.to_string() + .contains("parameter values are incompatible") + ); } #[tokio::test] async fn test_refresh_query_forbidden_includes_error_body() { let mock_server = MockServer::start().await; - mock_refresh_query_forbidden(123) - .mount(&mock_server) - .await; + mock_refresh_query_forbidden(123).mount(&mock_server).await; let client = RedashClient::new(mock_server.uri(), "test-key").unwrap(); let result = client.refresh_query(123, None).await; diff --git a/tests/common/mod.rs b/tests/common/mod.rs index f32a9cf..2399faa 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -5,8 +5,8 @@ use std::fs; use std::path::PathBuf; use tempfile::TempDir; -use wiremock::{MockServer, Mock, ResponseTemplate}; use wiremock::matchers::{method, path}; +use wiremock::{Mock, MockServer, ResponseTemplate}; pub struct TestContext { pub mock_server: MockServer, @@ -32,13 +32,7 @@ impl TestContext { self.mock_server.uri() } - pub fn create_query_files( - &self, - id: u64, - slug: &str, - sql: &str, - yaml_content: &str, - ) { + pub fn create_query_files(&self, id: u64, slug: &str, sql: &str, yaml_content: &str) { let sql_path = self.queries_dir.join(format!("{id}-{slug}.sql")); let yaml_path = self.queries_dir.join(format!("{id}-{slug}.yaml")); @@ -50,164 +44,148 @@ impl TestContext { pub fn mock_refresh_query(query_id: u64, job_id: &str) -> Mock { Mock::given(method("POST")) .and(path(format!("/api/queries/{query_id}/results"))) - .respond_with(ResponseTemplate::new(200).set_body_json( - serde_json::json!({ - "job": { - "id": job_id, - "status": 1, - "query_result_id": null, - "error": null - } - }) - )) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "job": { + "id": job_id, + "status": 1, + "query_result_id": null, + "error": null + } + }))) } pub fn mock_poll_job_pending(job_id: &str) -> Mock { Mock::given(method("GET")) .and(path(format!("/api/jobs/{job_id}"))) - .respond_with(ResponseTemplate::new(200).set_body_json( - serde_json::json!({ - "job": { - "id": job_id, - "status": 1, - "query_result_id": null, - "error": null - } - }) - )) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "job": { + "id": job_id, + "status": 1, + "query_result_id": null, + "error": null + } + }))) } pub fn mock_poll_job_success(job_id: &str, result_id: u64) -> Mock { Mock::given(method("GET")) .and(path(format!("/api/jobs/{job_id}"))) - .respond_with(ResponseTemplate::new(200).set_body_json( - serde_json::json!({ - "job": { - "id": job_id, - "status": 3, - "query_result_id": result_id, - "error": null - } - }) - )) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "job": { + "id": job_id, + "status": 3, + "query_result_id": result_id, + "error": null + } + }))) } pub fn mock_poll_job_failure(job_id: &str, error_msg: &str) -> Mock { Mock::given(method("GET")) .and(path(format!("/api/jobs/{job_id}"))) - .respond_with(ResponseTemplate::new(200).set_body_json( - serde_json::json!({ - "job": { - "id": job_id, - "status": 4, - "query_result_id": null, - "error": error_msg - } - }) - )) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "job": { + "id": job_id, + "status": 4, + "query_result_id": null, + "error": error_msg + } + }))) } pub fn mock_get_query_result(query_id: u64, result_id: u64) -> Mock { Mock::given(method("GET")) - .and(path(format!("/api/queries/{query_id}/results/{result_id}.json"))) - .respond_with(ResponseTemplate::new(200).set_body_json( - serde_json::json!({ - "query_result": { - "id": result_id, - "data": { - "columns": [ - {"name": "col1", "type": "string"}, - {"name": "col2", "type": "integer"} - ], - "rows": [ - {"col1": "value1", "col2": 123}, - {"col1": "value2", "col2": 456} - ] - }, - "runtime": 1.5, - "retrieved_at": "2026-01-21T10:00:00" - } - }) - )) + .and(path(format!( + "/api/queries/{query_id}/results/{result_id}.json" + ))) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "query_result": { + "id": result_id, + "data": { + "columns": [ + {"name": "col1", "type": "string"}, + {"name": "col2", "type": "integer"} + ], + "rows": [ + {"col1": "value1", "col2": 123}, + {"col1": "value2", "col2": 456} + ] + }, + "runtime": 1.5, + "retrieved_at": "2026-01-21T10:00:00" + } + }))) } pub fn mock_list_my_queries(page: u32, page_size: u32, total_count: u64) -> Mock { Mock::given(method("GET")) .and(path("/api/queries/my")) - .respond_with(ResponseTemplate::new(200).set_body_json( - serde_json::json!({ - "results": [], - "count": total_count, - "page": page, - "page_size": page_size - }) - )) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "results": [], + "count": total_count, + "page": page, + "page_size": page_size + }))) } pub fn mock_list_data_sources() -> Mock { Mock::given(method("GET")) .and(path("/api/data_sources")) - .respond_with(ResponseTemplate::new(200).set_body_json( - serde_json::json!([ - { - "id": 63, - "name": "Telemetry (BigQuery)", - "type": "bigquery", - "description": null, - "syntax": "sql", - "paused": 0, - "pause_reason": null, - "view_only": false, - "queue_name": "bq_queries", - "scheduled_queue_name": "bq_scheduled_queries", - "groups": {"2": false}, - "options": {} - }, - { - "id": 10, - "name": "Redash metadata", - "type": "pg", - "description": null, - "syntax": "sql", - "paused": 0, - "pause_reason": null, - "view_only": false, - "queue_name": "queries", - "scheduled_queue_name": "scheduled_queries", - "groups": {"2": false}, - "options": {} - } - ]) - )) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([ + { + "id": 63, + "name": "Telemetry (BigQuery)", + "type": "bigquery", + "description": null, + "syntax": "sql", + "paused": 0, + "pause_reason": null, + "view_only": false, + "queue_name": "bq_queries", + "scheduled_queue_name": "bq_scheduled_queries", + "groups": {"2": false}, + "options": {} + }, + { + "id": 10, + "name": "Redash metadata", + "type": "pg", + "description": null, + "syntax": "sql", + "paused": 0, + "pause_reason": null, + "view_only": false, + "queue_name": "queries", + "scheduled_queue_name": "scheduled_queries", + "groups": {"2": false}, + "options": {} + } + ]))) } pub fn mock_list_data_sources_empty() -> Mock { Mock::given(method("GET")) .and(path("/api/data_sources")) - .respond_with(ResponseTemplate::new(200).set_body_json( - serde_json::json!([]) - )) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([]))) } pub fn mock_get_data_source(data_source_id: u64) -> Mock { Mock::given(method("GET")) .and(path(format!("/api/data_sources/{data_source_id}"))) - .respond_with(ResponseTemplate::new(200).set_body_json( - serde_json::json!({ - "id": data_source_id, - "name": "Test Data Source", - "type": "bigquery", - "description": "Test description", - "syntax": "sql", - "paused": 0, - "pause_reason": null, - "view_only": false, - "queue_name": "queries", - "scheduled_queue_name": "scheduled_queries", - "groups": {}, - "options": {} - }) - )) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "id": data_source_id, + "name": "Test Data Source", + "type": "bigquery", + "description": "Test description", + "syntax": "sql", + "paused": 0, + "pause_reason": null, + "view_only": false, + "queue_name": "queries", + "scheduled_queue_name": "scheduled_queries", + "groups": {}, + "options": {} + }))) } pub fn mock_get_data_source_not_found(data_source_id: u64) -> Mock { @@ -219,27 +197,25 @@ pub fn mock_get_data_source_not_found(data_source_id: u64) -> Mock { pub fn mock_get_data_source_schema(data_source_id: u64) -> Mock { Mock::given(method("GET")) .and(path(format!("/api/data_sources/{data_source_id}/schema"))) - .respond_with(ResponseTemplate::new(200).set_body_json( - serde_json::json!({ - "schema": [ - { - "name": "table1", - "columns": [ - {"name": "col1", "type": "STRING"}, - {"name": "col2", "type": "INTEGER"}, - {"name": "col3", "type": "BOOLEAN"} - ] - }, - { - "name": "table2", - "columns": [ - {"name": "id", "type": "INTEGER"}, - {"name": "name", "type": "STRING"} - ] - } - ] - }) - )) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "schema": [ + { + "name": "table1", + "columns": [ + {"name": "col1", "type": "STRING"}, + {"name": "col2", "type": "INTEGER"}, + {"name": "col3", "type": "BOOLEAN"} + ] + }, + { + "name": "table2", + "columns": [ + {"name": "id", "type": "INTEGER"}, + {"name": "name", "type": "STRING"} + ] + } + ] + }))) } pub fn mock_get_data_source_schema_unauthorized(data_source_id: u64) -> Mock { @@ -251,27 +227,29 @@ pub fn mock_get_data_source_schema_unauthorized(data_source_id: u64) -> Mock { pub fn mock_get_query(query_id: u64, name: &str, is_archived: bool) -> Mock { Mock::given(method("GET")) .and(path(format!("/api/queries/{query_id}"))) - .respond_with(ResponseTemplate::new(200).set_body_json( - serde_json::json!({ - "id": query_id, - "name": name, - "description": null, - "query": "SELECT 1", - "data_source_id": 63, - "user": null, - "schedule": null, - "options": {"parameters": []}, - "visualizations": [], - "tags": null, - "is_archived": is_archived, - "is_draft": false, - "updated_at": "2026-01-21T10:00:00", - "created_at": "2026-01-21T10:00:00" - }) - )) -} - -pub fn mock_get_query_with_parameters(query_id: u64, name: &str, parameters: &[(&str, &str)]) -> Mock { + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "id": query_id, + "name": name, + "description": null, + "query": "SELECT 1", + "data_source_id": 63, + "user": null, + "schedule": null, + "options": {"parameters": []}, + "visualizations": [], + "tags": null, + "is_archived": is_archived, + "is_draft": false, + "updated_at": "2026-01-21T10:00:00", + "created_at": "2026-01-21T10:00:00" + }))) +} + +pub fn mock_get_query_with_parameters( + query_id: u64, + name: &str, + parameters: &[(&str, &str)], +) -> Mock { let params: Vec = parameters .iter() .map(|(param_name, param_type)| { @@ -285,115 +263,105 @@ pub fn mock_get_query_with_parameters(query_id: u64, name: &str, parameters: &[( Mock::given(method("GET")) .and(path(format!("/api/queries/{query_id}"))) - .respond_with(ResponseTemplate::new(200).set_body_json( - serde_json::json!({ - "id": query_id, - "name": name, - "description": null, - "query": "SELECT 1", - "data_source_id": 63, - "user": null, - "schedule": null, - "options": {"parameters": params}, - "visualizations": [], - "tags": null, - "is_archived": false, - "is_draft": false, - "updated_at": "2026-01-21T10:00:00", - "created_at": "2026-01-21T10:00:00" - }) - )) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "id": query_id, + "name": name, + "description": null, + "query": "SELECT 1", + "data_source_id": 63, + "user": null, + "schedule": null, + "options": {"parameters": params}, + "visualizations": [], + "tags": null, + "is_archived": false, + "is_draft": false, + "updated_at": "2026-01-21T10:00:00", + "created_at": "2026-01-21T10:00:00" + }))) } pub fn mock_get_query_with_table_viz(query_id: u64, name: &str) -> Mock { Mock::given(method("GET")) .and(path(format!("/api/queries/{query_id}"))) - .respond_with(ResponseTemplate::new(200).set_body_json( - serde_json::json!({ - "id": query_id, - "name": name, - "description": null, - "query": "SELECT 1", - "data_source_id": 63, - "user": null, - "schedule": null, - "options": {"parameters": []}, - "visualizations": [ - { - "id": 99999, - "name": "Table", - "type": "TABLE", - "options": {}, - "description": null - } - ], - "tags": null, - "is_archived": false, - "is_draft": false, - "updated_at": "2026-01-21T10:00:00", - "created_at": "2026-01-21T10:00:00" - }) - )) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "id": query_id, + "name": name, + "description": null, + "query": "SELECT 1", + "data_source_id": 63, + "user": null, + "schedule": null, + "options": {"parameters": []}, + "visualizations": [ + { + "id": 99999, + "name": "Table", + "type": "TABLE", + "options": {}, + "description": null + } + ], + "tags": null, + "is_archived": false, + "is_draft": false, + "updated_at": "2026-01-21T10:00:00", + "created_at": "2026-01-21T10:00:00" + }))) } pub fn mock_update_visualization(viz_id: u64) -> Mock { Mock::given(method("POST")) .and(path(format!("/api/visualizations/{viz_id}"))) - .respond_with(ResponseTemplate::new(200).set_body_json( - serde_json::json!({ - "id": viz_id, - "name": "Table", - "type": "TABLE", - "options": {}, - "description": null - }) - )) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "id": viz_id, + "name": "Table", + "type": "TABLE", + "options": {}, + "description": null + }))) } pub fn mock_archive_query(query_id: u64, name: &str) -> Mock { Mock::given(method("POST")) .and(path(format!("/api/queries/{query_id}"))) - .respond_with(ResponseTemplate::new(200).set_body_json( - serde_json::json!({ - "id": query_id, - "name": name, - "description": null, - "query": "SELECT 1", - "data_source_id": 63, - "user": null, - "schedule": null, - "options": {"parameters": []}, - "visualizations": [], - "tags": null, - "is_archived": true, - "is_draft": false, - "updated_at": "2026-01-21T10:00:00", - "created_at": "2026-01-21T10:00:00" - }) - )) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "id": query_id, + "name": name, + "description": null, + "query": "SELECT 1", + "data_source_id": 63, + "user": null, + "schedule": null, + "options": {"parameters": []}, + "visualizations": [], + "tags": null, + "is_archived": true, + "is_draft": false, + "updated_at": "2026-01-21T10:00:00", + "created_at": "2026-01-21T10:00:00" + }))) } pub fn mock_unarchive_query(query_id: u64, name: &str) -> Mock { Mock::given(method("POST")) .and(path(format!("/api/queries/{query_id}"))) - .respond_with(ResponseTemplate::new(200).set_body_json( - serde_json::json!({ - "id": query_id, - "name": name, - "description": null, - "query": "SELECT 1", - "data_source_id": 63, - "user": null, - "schedule": null, - "options": {"parameters": []}, - "visualizations": [], - "tags": null, - "is_archived": false, - "is_draft": false, - "updated_at": "2026-01-21T10:00:00", - "created_at": "2026-01-21T10:00:00" - }) - )) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "id": query_id, + "name": name, + "description": null, + "query": "SELECT 1", + "data_source_id": 63, + "user": null, + "schedule": null, + "options": {"parameters": []}, + "visualizations": [], + "tags": null, + "is_archived": false, + "is_draft": false, + "updated_at": "2026-01-21T10:00:00", + "created_at": "2026-01-21T10:00:00" + }))) } pub fn mock_archive_query_not_found(query_id: u64) -> Mock { @@ -411,79 +379,71 @@ pub fn mock_unarchive_query_forbidden(query_id: u64) -> Mock { pub fn mock_create_query(id: u64, name: &str) -> Mock { Mock::given(method("POST")) .and(path("/api/queries")) - .respond_with(ResponseTemplate::new(200).set_body_json( - serde_json::json!({ - "id": id, - "name": name, - "description": null, - "query": "SELECT 1", - "data_source_id": 63, - "user": null, - "schedule": null, - "options": {"parameters": []}, - "visualizations": [], - "tags": null, - "is_archived": false, - "is_draft": false, - "updated_at": "2026-01-21T10:00:00", - "created_at": "2026-01-21T10:00:00" - }) - )) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "id": id, + "name": name, + "description": null, + "query": "SELECT 1", + "data_source_id": 63, + "user": null, + "schedule": null, + "options": {"parameters": []}, + "visualizations": [], + "tags": null, + "is_archived": false, + "is_draft": false, + "updated_at": "2026-01-21T10:00:00", + "created_at": "2026-01-21T10:00:00" + }))) } pub fn mock_create_dashboard(id: u64, name: &str, slug: &str) -> Mock { Mock::given(method("POST")) .and(path("/api/dashboards")) - .respond_with(ResponseTemplate::new(200).set_body_json( - serde_json::json!({ - "id": id, - "name": name, - "slug": slug, - "user_id": 530, - "is_archived": false, - "is_draft": true, - "dashboard_filters_enabled": false, - "tags": [], - "widgets": null - }) - )) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "id": id, + "name": name, + "slug": slug, + "user_id": 530, + "is_archived": false, + "is_draft": true, + "dashboard_filters_enabled": false, + "tags": [], + "widgets": null + }))) } pub fn mock_list_favorite_dashboards(count: u64) -> Mock { Mock::given(method("GET")) .and(path("/api/dashboards/favorites")) - .respond_with(ResponseTemplate::new(200).set_body_json( - serde_json::json!({ - "results": [ - { - "id": 2570, - "name": "Firefox Desktop on SteamOS", - "slug": "firefox-desktop-on-steamos", - "is_draft": false, - "is_archived": false - }, - { - "id": 2558, - "name": "Test Dashboard", - "slug": "test-dashboard", - "is_draft": false, - "is_archived": false - } - ], - "count": count - }) - )) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "results": [ + { + "id": 2570, + "name": "Firefox Desktop on SteamOS", + "slug": "firefox-desktop-on-steamos", + "is_draft": false, + "is_archived": false + }, + { + "id": 2558, + "name": "Test Dashboard", + "slug": "test-dashboard", + "is_draft": false, + "is_archived": false + } + ], + "count": count + }))) } pub fn mock_list_favorite_dashboards_empty() -> Mock { Mock::given(method("GET")) .and(path("/api/dashboards/favorites")) - .respond_with(ResponseTemplate::new(200).set_body_json( - serde_json::json!({ - "results": [], - "count": 0 - }) - )) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "results": [], + "count": 0 + }))) } pub fn mock_get_dashboard(id: u64, name: &str, is_archived: bool) -> Mock { @@ -494,37 +454,33 @@ pub fn mock_get_dashboard(id: u64, name: &str, is_archived: bool) -> Mock { pub fn mock_get_dashboard_with_slug(id: u64, name: &str, slug: &str, is_archived: bool) -> Mock { Mock::given(method("GET")) .and(path(format!("/api/dashboards/{slug}"))) - .respond_with(ResponseTemplate::new(200).set_body_json( - serde_json::json!({ - "id": id, - "name": name, - "slug": slug, - "user_id": 530, - "is_archived": is_archived, - "is_draft": false, - "dashboard_filters_enabled": false, - "tags": [], - "widgets": [] - }) - )) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "id": id, + "name": name, + "slug": slug, + "user_id": 530, + "is_archived": is_archived, + "is_draft": false, + "dashboard_filters_enabled": false, + "tags": [], + "widgets": [] + }))) } pub fn mock_get_dashboard_by_id(id: u64, name: &str, slug: &str, is_archived: bool) -> Mock { Mock::given(method("GET")) .and(path(format!("/api/dashboards/{id}"))) - .respond_with(ResponseTemplate::new(200).set_body_json( - serde_json::json!({ - "id": id, - "name": name, - "slug": slug, - "user_id": 530, - "is_archived": is_archived, - "is_draft": false, - "dashboard_filters_enabled": false, - "tags": [], - "widgets": [] - }) - )) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "id": id, + "name": name, + "slug": slug, + "user_id": 530, + "is_archived": is_archived, + "is_draft": false, + "dashboard_filters_enabled": false, + "tags": [], + "widgets": [] + }))) } pub fn mock_get_dashboard_not_found(slug: &str) -> Mock { @@ -536,19 +492,17 @@ pub fn mock_get_dashboard_not_found(slug: &str) -> Mock { pub fn mock_update_dashboard(id: u64, name: &str) -> Mock { Mock::given(method("POST")) .and(path(format!("/api/dashboards/{id}"))) - .respond_with(ResponseTemplate::new(200).set_body_json( - serde_json::json!({ - "id": id, - "name": name, - "slug": name.to_lowercase().replace(' ', "-"), - "user_id": 530, - "is_archived": false, - "is_draft": false, - "dashboard_filters_enabled": false, - "tags": [], - "widgets": [] - }) - )) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "id": id, + "name": name, + "slug": name.to_lowercase().replace(' ', "-"), + "user_id": 530, + "is_archived": false, + "is_draft": false, + "dashboard_filters_enabled": false, + "tags": [], + "widgets": [] + }))) } pub fn mock_archive_dashboard(id: u64) -> Mock { @@ -566,19 +520,17 @@ pub fn mock_archive_dashboard_not_found(id: u64) -> Mock { pub fn mock_unarchive_dashboard(id: u64, name: &str) -> Mock { Mock::given(method("POST")) .and(path(format!("/api/dashboards/{id}"))) - .respond_with(ResponseTemplate::new(200).set_body_json( - serde_json::json!({ - "id": id, - "name": name, - "slug": name.to_lowercase().replace(' ', "-"), - "user_id": 530, - "is_archived": false, - "is_draft": false, - "dashboard_filters_enabled": false, - "tags": [], - "widgets": [] - }) - )) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "id": id, + "name": name, + "slug": name.to_lowercase().replace(' ', "-"), + "user_id": 530, + "is_archived": false, + "is_draft": false, + "dashboard_filters_enabled": false, + "tags": [], + "widgets": [] + }))) } pub fn mock_unarchive_dashboard_forbidden(id: u64) -> Mock { @@ -590,42 +542,38 @@ pub fn mock_unarchive_dashboard_forbidden(id: u64) -> Mock { pub fn mock_create_widget(dashboard_id: u64, widget_id: u64) -> Mock { Mock::given(method("POST")) .and(path("/api/widgets")) - .respond_with(ResponseTemplate::new(200).set_body_json( - serde_json::json!({ - "id": widget_id, - "dashboard_id": dashboard_id, - "width": 1, - "visualization_id": null, - "visualization": null, - "text": "Test Widget", - "options": { - "position": { - "col": 0, - "row": 0, - "sizeX": 3, - "sizeY": 2 - } + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "id": widget_id, + "dashboard_id": dashboard_id, + "width": 1, + "visualization_id": null, + "visualization": null, + "text": "Test Widget", + "options": { + "position": { + "col": 0, + "row": 0, + "sizeX": 3, + "sizeY": 2 } - }) - )) + } + }))) } pub fn mock_update_widget(widget_id: u64, dashboard_id: u64) -> Mock { Mock::given(method("POST")) .and(path(format!("/api/widgets/{widget_id}"))) - .respond_with(ResponseTemplate::new(200).set_body_json( - serde_json::json!({ - "id": widget_id, - "dashboard_id": dashboard_id, - "width": 1, - "visualization_id": null, - "visualization": null, - "text": "", - "options": { - "position": { "col": 0, "row": 0, "sizeX": 3, "sizeY": 2 } - } - }) - )) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "id": widget_id, + "dashboard_id": dashboard_id, + "width": 1, + "visualization_id": null, + "visualization": null, + "text": "", + "options": { + "position": { "col": 0, "row": 0, "sizeX": 3, "sizeY": 2 } + } + }))) } pub fn mock_delete_widget(widget_id: u64) -> Mock { @@ -644,8 +592,7 @@ pub fn mock_refresh_query_bad_request(query_id: u64, error_message: &str) -> Moc Mock::given(method("POST")) .and(path(format!("/api/queries/{query_id}/results"))) .respond_with( - ResponseTemplate::new(400) - .set_body_json(serde_json::json!({"message": error_message})) + ResponseTemplate::new(400).set_body_json(serde_json::json!({"message": error_message})), ) } @@ -660,66 +607,60 @@ pub fn mock_refresh_query_forbidden(query_id: u64) -> Mock { .and(path(format!("/api/queries/{query_id}/results"))) .respond_with( ResponseTemplate::new(403) - .set_body_json(serde_json::json!({"message": "Access denied"})) + .set_body_json(serde_json::json!({"message": "Access denied"})), ) } pub fn mock_create_visualization(viz_id: u64, name: &str) -> Mock { Mock::given(method("POST")) .and(path("/api/visualizations")) - .respond_with(ResponseTemplate::new(200).set_body_json( - serde_json::json!({ - "id": viz_id, - "name": name, - "type": "CHART", - "options": {}, - "description": null - }) - )) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "id": viz_id, + "name": name, + "type": "CHART", + "options": {}, + "description": null + }))) } pub fn mock_update_query_with_vizs(query_id: u64, name: &str, vizs: &serde_json::Value) -> Mock { Mock::given(method("POST")) .and(path(format!("/api/queries/{query_id}"))) - .respond_with(ResponseTemplate::new(200).set_body_json( - serde_json::json!({ - "id": query_id, - "name": name, - "description": null, - "query": "SELECT 1", - "data_source_id": 63, - "user": null, - "schedule": null, - "options": {"parameters": []}, - "visualizations": vizs, - "tags": null, - "is_archived": false, - "is_draft": false, - "updated_at": "2026-01-21T10:00:00", - "created_at": "2026-01-21T10:00:00" - }) - )) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "id": query_id, + "name": name, + "description": null, + "query": "SELECT 1", + "data_source_id": 63, + "user": null, + "schedule": null, + "options": {"parameters": []}, + "visualizations": vizs, + "tags": null, + "is_archived": false, + "is_draft": false, + "updated_at": "2026-01-21T10:00:00", + "created_at": "2026-01-21T10:00:00" + }))) } pub fn mock_get_query_with_vizs(query_id: u64, name: &str, vizs: &serde_json::Value) -> Mock { Mock::given(method("GET")) .and(path(format!("/api/queries/{query_id}"))) - .respond_with(ResponseTemplate::new(200).set_body_json( - serde_json::json!({ - "id": query_id, - "name": name, - "description": null, - "query": "SELECT 1", - "data_source_id": 63, - "user": null, - "schedule": null, - "options": {"parameters": []}, - "visualizations": vizs, - "tags": null, - "is_archived": false, - "is_draft": false, - "updated_at": "2026-01-21T10:00:00", - "created_at": "2026-01-21T10:00:00" - }) - )) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "id": query_id, + "name": name, + "description": null, + "query": "SELECT 1", + "data_source_id": 63, + "user": null, + "schedule": null, + "options": {"parameters": []}, + "visualizations": vizs, + "tags": null, + "is_archived": false, + "is_draft": false, + "updated_at": "2026-01-21T10:00:00", + "created_at": "2026-01-21T10:00:00" + }))) } diff --git a/tests/dashboard_commands.rs b/tests/dashboard_commands.rs index 86e4a87..ab57505 100644 --- a/tests/dashboard_commands.rs +++ b/tests/dashboard_commands.rs @@ -3,12 +3,12 @@ mod common; -use stmo_cli::api::RedashClient; use common::*; -use tempfile::TempDir; use std::env; -use tokio::sync::Mutex; use std::sync::OnceLock; +use stmo_cli::api::RedashClient; +use tempfile::TempDir; +use tokio::sync::Mutex; static TEST_MUTEX: OnceLock> = OnceLock::new(); @@ -55,7 +55,14 @@ async fn test_fetch_with_all_failures_returns_error() { let client = RedashClient::new(mock_server.uri(), "test-key").unwrap(); - let result = stmo_cli::commands::dashboards::fetch(&client, vec!["firefox-desktop-on-steamos".to_string(), "test-dashboard".to_string()]).await; + let result = stmo_cli::commands::dashboards::fetch( + &client, + vec![ + "firefox-desktop-on-steamos".to_string(), + "test-dashboard".to_string(), + ], + ) + .await; assert!(result.is_err()); let error = result.unwrap_err(); @@ -80,13 +87,26 @@ async fn test_fetch_with_partial_failures_returns_error() { let client = RedashClient::new(mock_server.uri(), "test-key").unwrap(); - let result = stmo_cli::commands::dashboards::fetch(&client, vec!["firefox-desktop-on-steamos".to_string(), "test-dashboard".to_string()]).await; + let result = stmo_cli::commands::dashboards::fetch( + &client, + vec![ + "firefox-desktop-on-steamos".to_string(), + "test-dashboard".to_string(), + ], + ) + .await; assert!(result.is_err()); let error = result.unwrap_err(); let error_msg = error.to_string(); - assert!(error_msg.contains("dashboard(s) failed to fetch"), "Error was: {error_msg}"); - assert!(error_msg.contains("test-dashboard"), "Error was: {error_msg}"); + assert!( + error_msg.contains("dashboard(s) failed to fetch"), + "Error was: {error_msg}" + ); + assert!( + error_msg.contains("test-dashboard"), + "Error was: {error_msg}" + ); } #[tokio::test] @@ -105,7 +125,14 @@ async fn test_fetch_with_all_success_returns_ok() { let client = RedashClient::new(mock_server.uri(), "test-key").unwrap(); - let result = stmo_cli::commands::dashboards::fetch(&client, vec!["firefox-desktop-on-steamos".to_string(), "test-dashboard".to_string()]).await; + let result = stmo_cli::commands::dashboards::fetch( + &client, + vec![ + "firefox-desktop-on-steamos".to_string(), + "test-dashboard".to_string(), + ], + ) + .await; assert!(result.is_ok()); @@ -119,7 +146,8 @@ async fn test_fetch_with_all_success_returns_ok() { assert_eq!(files.len(), 2); - let yaml_files: Vec<_> = files.iter() + let yaml_files: Vec<_> = files + .iter() .filter(|e| e.path().extension().is_some_and(|ext| ext == "yaml")) .collect(); @@ -142,7 +170,14 @@ async fn test_archive_with_all_failures_returns_error() { let client = RedashClient::new(mock_server.uri(), "test-key").unwrap(); - let result = stmo_cli::commands::dashboards::archive(&client, vec!["firefox-desktop-on-steamos".to_string(), "test-dashboard".to_string()]).await; + let result = stmo_cli::commands::dashboards::archive( + &client, + vec![ + "firefox-desktop-on-steamos".to_string(), + "test-dashboard".to_string(), + ], + ) + .await; assert!(result.is_err()); let error = result.unwrap_err(); @@ -173,11 +208,22 @@ async fn test_unarchive_with_failures_returns_error() { let client = RedashClient::new(mock_server.uri(), "test-key").unwrap(); - let result = stmo_cli::commands::dashboards::unarchive(&client, vec!["firefox-desktop-on-steamos".to_string(), "test-dashboard".to_string()]).await; + let result = stmo_cli::commands::dashboards::unarchive( + &client, + vec![ + "firefox-desktop-on-steamos".to_string(), + "test-dashboard".to_string(), + ], + ) + .await; assert!(result.is_err()); let error = result.unwrap_err(); - assert!(error.to_string().contains("1 dashboard(s) failed to unarchive")); + assert!( + error + .to_string() + .contains("1 dashboard(s) failed to unarchive") + ); assert!(error.to_string().contains("firefox-desktop-on-steamos")); } @@ -191,14 +237,18 @@ async fn test_fetch_with_triple_dash_slug() { 2_006_698, "Bug 2006698 - ccov build regression", "bug-2006698---ccov-build-regression", - false + false, ) - .mount(&mock_server) - .await; + .mount(&mock_server) + .await; let client = RedashClient::new(mock_server.uri(), "test-key").unwrap(); - let result = stmo_cli::commands::dashboards::fetch(&client, vec!["bug-2006698---ccov-build-regression".to_string()]).await; + let result = stmo_cli::commands::dashboards::fetch( + &client, + vec!["bug-2006698---ccov-build-regression".to_string()], + ) + .await; assert!(result.is_ok()); @@ -206,7 +256,10 @@ async fn test_fetch_with_triple_dash_slug() { assert!(dashboards_dir.exists()); let expected_file = dashboards_dir.join("2006698-bug-2006698---ccov-build-regression.yaml"); - assert!(expected_file.exists(), "Expected file {expected_file:?} to exist"); + assert!( + expected_file.exists(), + "Expected file {expected_file:?} to exist" + ); let yaml_content = std::fs::read_to_string(&expected_file).unwrap(); assert!(yaml_content.contains("slug: bug-2006698---ccov-build-regression")); @@ -223,10 +276,10 @@ async fn test_deploy_with_triple_dash_slug() { 2_006_698, "Bug 2006698 - ccov build regression", "bug-2006698---ccov-build-regression", - false + false, ) - .mount(&mock_server) - .await; + .mount(&mock_server) + .await; mock_update_dashboard(2_006_698, "Bug 2006698 - ccov build regression") .mount(&mock_server) @@ -237,10 +290,10 @@ async fn test_deploy_with_triple_dash_slug() { 2_006_698, "Bug 2006698 - ccov build regression", "bug-2006698---ccov-build-regression", - false + false, ) - .mount(&mock_server) - .await; + .mount(&mock_server) + .await; let client = RedashClient::new(mock_server.uri(), "test-key").unwrap(); @@ -256,9 +309,18 @@ dashboard_filters_enabled: false tags: [] widgets: [] "; - std::fs::write("dashboards/2006698-bug-2006698---ccov-build-regression.yaml", yaml_content).unwrap(); + std::fs::write( + "dashboards/2006698-bug-2006698---ccov-build-regression.yaml", + yaml_content, + ) + .unwrap(); - let result = stmo_cli::commands::dashboards::deploy(&client, vec!["bug-2006698---ccov-build-regression".to_string()], false).await; + let result = stmo_cli::commands::dashboards::deploy( + &client, + vec!["bug-2006698---ccov-build-regression".to_string()], + false, + ) + .await; assert!(result.is_ok(), "Deploy failed: {:?}", result.err()); } @@ -273,14 +335,12 @@ async fn test_archive_with_triple_dash_slug() { 2_006_698, "Bug 2006698 - ccov build regression", "bug-2006698---ccov-build-regression", - false + false, ) - .mount(&mock_server) - .await; + .mount(&mock_server) + .await; - mock_archive_dashboard(2_006_698) - .mount(&mock_server) - .await; + mock_archive_dashboard(2_006_698).mount(&mock_server).await; let client = RedashClient::new(mock_server.uri(), "test-key").unwrap(); @@ -290,10 +350,17 @@ async fn test_archive_with_triple_dash_slug() { assert!(std::path::Path::new(yaml_file).exists()); - let result = stmo_cli::commands::dashboards::archive(&client, vec!["bug-2006698---ccov-build-regression".to_string()]).await; + let result = stmo_cli::commands::dashboards::archive( + &client, + vec!["bug-2006698---ccov-build-regression".to_string()], + ) + .await; assert!(result.is_ok()); - assert!(!std::path::Path::new(yaml_file).exists(), "File should be deleted after archiving"); + assert!( + !std::path::Path::new(yaml_file).exists(), + "File should be deleted after archiving" + ); } #[tokio::test] @@ -335,17 +402,26 @@ widgets: [] "; std::fs::write("dashboards/0-my-new-dashboard.yaml", yaml_content).unwrap(); - let result = stmo_cli::commands::dashboards::deploy(&client, vec!["my-new-dashboard".to_string()], false).await; + let result = stmo_cli::commands::dashboards::deploy( + &client, + vec!["my-new-dashboard".to_string()], + false, + ) + .await; assert!(result.is_ok(), "Deploy failed: {:?}", result.err()); // Old file should be deleted - assert!(!std::path::Path::new("dashboards/0-my-new-dashboard.yaml").exists(), - "Old 0-*.yaml file should be removed after creation"); + assert!( + !std::path::Path::new("dashboards/0-my-new-dashboard.yaml").exists(), + "Old 0-*.yaml file should be removed after creation" + ); // New file with server-assigned ID should exist - assert!(std::path::Path::new("dashboards/2621-my-new-dashboard.yaml").exists(), - "New file with server ID should be created"); + assert!( + std::path::Path::new("dashboards/2621-my-new-dashboard.yaml").exists(), + "New file with server ID should be created" + ); } #[tokio::test] @@ -362,9 +438,13 @@ async fn test_deploy_auto_populates_parameter_mappings() { .mount(&mock_server) .await; - mock_get_query_with_parameters(query_id, "My Query", &[("channel", "enum"), ("date", "date")]) - .mount(&mock_server) - .await; + mock_get_query_with_parameters( + query_id, + "My Query", + &[("channel", "enum"), ("date", "date")], + ) + .mount(&mock_server) + .await; mock_create_widget(dashboard_id, 99001) .mount(&mock_server) @@ -382,7 +462,8 @@ async fn test_deploy_auto_populates_parameter_mappings() { std::fs::create_dir_all("dashboards").unwrap(); - let yaml_content = format!("id: {dashboard_id} + let yaml_content = format!( + "id: {dashboard_id} name: My Parameterized Dashboard slug: {slug} user_id: 530 @@ -402,14 +483,16 @@ widgets: row: 0 sizeX: 3 sizeY: 8 -"); - std::fs::write(format!("dashboards/{dashboard_id}-{slug}.yaml"), yaml_content).unwrap(); +" + ); + std::fs::write( + format!("dashboards/{dashboard_id}-{slug}.yaml"), + yaml_content, + ) + .unwrap(); - let result = stmo_cli::commands::dashboards::deploy( - &client, - vec![slug.to_string()], - false, - ).await; + let result = + stmo_cli::commands::dashboards::deploy(&client, vec![slug.to_string()], false).await; assert!(result.is_ok(), "Deploy failed: {:?}", result.err()); @@ -423,7 +506,10 @@ widgets: let body: serde_json::Value = serde_json::from_slice(&widget_create_req.body).unwrap(); let param_mappings = &body["options"]["parameterMappings"]; - assert!(param_mappings.is_object(), "parameterMappings should be an object, got: {param_mappings}"); + assert!( + param_mappings.is_object(), + "parameterMappings should be an object, got: {param_mappings}" + ); assert_eq!(param_mappings["channel"]["type"], "dashboard-level"); assert_eq!(param_mappings["channel"]["mapTo"], "channel"); assert_eq!(param_mappings["date"]["type"], "dashboard-level"); @@ -432,8 +518,7 @@ widgets: let dashboard_update_req = received .iter() .find(|r| { - r.method.as_str() == "POST" - && r.url.path() == format!("/api/dashboards/{dashboard_id}") + r.method.as_str() == "POST" && r.url.path() == format!("/api/dashboards/{dashboard_id}") }) .expect("Expected dashboard update request"); @@ -483,7 +568,8 @@ async fn test_deploy_resolves_visualization_id_from_query_and_name() { std::fs::create_dir_all("dashboards").unwrap(); - let yaml_content = format!("id: {dashboard_id} + let yaml_content = format!( + "id: {dashboard_id} name: My Dashboard slug: {slug} user_id: 530 @@ -501,14 +587,16 @@ widgets: row: 0 sizeX: 3 sizeY: 8 -"); - std::fs::write(format!("dashboards/{dashboard_id}-{slug}.yaml"), yaml_content).unwrap(); +" + ); + std::fs::write( + format!("dashboards/{dashboard_id}-{slug}.yaml"), + yaml_content, + ) + .unwrap(); - let result = stmo_cli::commands::dashboards::deploy( - &client, - vec![slug.to_string()], - false, - ).await; + let result = + stmo_cli::commands::dashboards::deploy(&client, vec![slug.to_string()], false).await; assert!(result.is_ok(), "Deploy failed: {:?}", result.err()); @@ -552,7 +640,8 @@ async fn test_deploy_fails_when_visualization_name_not_found() { std::fs::create_dir_all("dashboards").unwrap(); - let yaml_content = format!("id: {dashboard_id} + let yaml_content = format!( + "id: {dashboard_id} name: My Dashboard slug: {slug} user_id: 530 @@ -570,14 +659,16 @@ widgets: row: 0 sizeX: 3 sizeY: 8 -"); - std::fs::write(format!("dashboards/{dashboard_id}-{slug}.yaml"), yaml_content).unwrap(); +" + ); + std::fs::write( + format!("dashboards/{dashboard_id}-{slug}.yaml"), + yaml_content, + ) + .unwrap(); - let result = stmo_cli::commands::dashboards::deploy( - &client, - vec![slug.to_string()], - false, - ).await; + let result = + stmo_cli::commands::dashboards::deploy(&client, vec![slug.to_string()], false).await; assert!(result.is_err(), "Expected deploy to fail"); let err = result.unwrap_err().to_string(); @@ -648,7 +739,8 @@ async fn test_deploy_updates_existing_widgets() { std::fs::create_dir_all("dashboards").unwrap(); - let yaml_content = format!("id: {dashboard_id} + let yaml_content = format!( + "id: {dashboard_id} name: My Dashboard slug: {slug} user_id: 530 @@ -666,31 +758,39 @@ widgets: row: 5 sizeX: 6 sizeY: 4 -"); - std::fs::write(format!("dashboards/{dashboard_id}-{slug}.yaml"), yaml_content).unwrap(); +" + ); + std::fs::write( + format!("dashboards/{dashboard_id}-{slug}.yaml"), + yaml_content, + ) + .unwrap(); - let result = stmo_cli::commands::dashboards::deploy( - &client, - vec![slug.to_string()], - false, - ).await; + let result = + stmo_cli::commands::dashboards::deploy(&client, vec![slug.to_string()], false).await; assert!(result.is_ok(), "Deploy failed: {:?}", result.err()); let received = mock_server.received_requests().await.unwrap(); - let widget_update_req = received - .iter() - .find(|r| r.method.as_str() == "POST" && r.url.path() == format!("/api/widgets/{widget_id}")); + let widget_update_req = received.iter().find(|r| { + r.method.as_str() == "POST" && r.url.path() == format!("/api/widgets/{widget_id}") + }); assert!( widget_update_req.is_some(), "Expected POST /api/widgets/{widget_id} but got: {:?}", - received.iter().map(|r| format!("{} {}", r.method, r.url.path())).collect::>() + received + .iter() + .map(|r| format!("{} {}", r.method, r.url.path())) + .collect::>() ); let body: serde_json::Value = serde_json::from_slice(&widget_update_req.unwrap().body).unwrap(); - assert_eq!(body["visualization_id"], 55557, "visualization_id should resolve to Updated Chart"); + assert_eq!( + body["visualization_id"], 55557, + "visualization_id should resolve to Updated Chart" + ); assert_eq!(body["options"]["position"]["col"], 3); assert_eq!(body["options"]["position"]["row"], 5); } diff --git a/tests/deploy_commands.rs b/tests/deploy_commands.rs index 705ae14..5af2497 100644 --- a/tests/deploy_commands.rs +++ b/tests/deploy_commands.rs @@ -3,12 +3,12 @@ mod common; -use stmo_cli::api::RedashClient; use common::*; -use tempfile::TempDir; use std::env; -use tokio::sync::Mutex; use std::sync::OnceLock; +use stmo_cli::api::RedashClient; +use tempfile::TempDir; +use tokio::sync::Mutex; static TEST_MUTEX: OnceLock> = OnceLock::new(); @@ -86,7 +86,10 @@ async fn test_deploy_new_query_with_id_zero() { ); let yaml_content = std::fs::read_to_string("queries/42-test-query.yaml").unwrap(); - assert!(yaml_content.contains("id: 42"), "YAML should contain the new ID"); + assert!( + yaml_content.contains("id: 42"), + "YAML should contain the new ID" + ); } #[tokio::test] diff --git a/tests/proxy_tunnel.rs b/tests/proxy_tunnel.rs index 8b84390..8cd6842 100644 --- a/tests/proxy_tunnel.rs +++ b/tests/proxy_tunnel.rs @@ -4,8 +4,7 @@ /// Skipped when no HTTPS proxy is configured. #[tokio::test] async fn https_through_connect_proxy() { - let Ok(proxy_url) = std::env::var("https_proxy") - .or_else(|_| std::env::var("HTTPS_PROXY")) + let Ok(proxy_url) = std::env::var("https_proxy").or_else(|_| std::env::var("HTTPS_PROXY")) else { return; }; From dc9bd20088a31e29a6ccb59b6b4a69392d0f3749 Mon Sep 17 00:00:00 2001 From: Johan Lorenzo Date: Fri, 22 May 2026 08:53:25 +0200 Subject: [PATCH 3/3] Fix clippy too_many_lines errors exposed by rustfmt --- src/commands/dashboards.rs | 85 ++++++++++++++------------------ src/main.rs | 99 +++++++++++++++++++------------------- 2 files changed, 86 insertions(+), 98 deletions(-) diff --git a/src/commands/dashboards.rs b/src/commands/dashboards.rs index 4beb840..d0a00da 100644 --- a/src/commands/dashboards.rs +++ b/src/commands/dashboards.rs @@ -331,6 +331,29 @@ fn find_dashboard_yaml(dashboard_slug: &str) -> Result { Ok(yaml_files[0].path()) } +async fn resolve_widget_options( + client: &RedashClient, + widget: &WidgetMetadata, + query_cache: &mut HashMap, +) -> Result<(crate::models::WidgetOptions, bool)> { + let mut options = widget.options.clone(); + let has_params = if let Some(query_id) = widget.query_id + && let Some(mappings) = auto_populate_parameter_mappings( + client, + query_id, + options.parameter_mappings.as_ref(), + query_cache, + ) + .await? + { + options.parameter_mappings = Some(mappings); + true + } else { + false + }; + Ok((options, has_params)) +} + async fn deploy_single_dashboard(client: &RedashClient, dashboard_slug: &str) -> Result { let yaml_path = find_dashboard_yaml(dashboard_slug)?; let yaml_content = fs::read_to_string(&yaml_path) @@ -377,56 +400,22 @@ async fn deploy_single_dashboard(client: &RedashClient, dashboard_slug: &str) -> let mut any_widget_has_params = false; for widget in &local_metadata.widgets { + let (options, has_params) = + resolve_widget_options(client, widget, &mut query_cache).await?; + if has_params { + any_widget_has_params = true; + } + let payload = CreateWidget { + dashboard_id: server_dashboard_id, + visualization_id: resolve_visualization_id(client, widget, &mut query_cache).await?, + text: widget.text.clone(), + options, + width: if widget.id == 0 { 1 } else { widget.width }, + }; if widget.id == 0 { - let mut options = widget.options.clone(); - - if let Some(query_id) = widget.query_id - && let Some(mappings) = auto_populate_parameter_mappings( - client, - query_id, - options.parameter_mappings.as_ref(), - &mut query_cache, - ) - .await? - { - options.parameter_mappings = Some(mappings); - any_widget_has_params = true; - } - - let create_widget = CreateWidget { - dashboard_id: server_dashboard_id, - visualization_id: resolve_visualization_id(client, widget, &mut query_cache) - .await?, - text: widget.text.clone(), - width: 1, - options, - }; - client.create_widget(&create_widget).await?; + client.create_widget(&payload).await?; } else { - let mut options = widget.options.clone(); - - if let Some(query_id) = widget.query_id - && let Some(mappings) = auto_populate_parameter_mappings( - client, - query_id, - options.parameter_mappings.as_ref(), - &mut query_cache, - ) - .await? - { - options.parameter_mappings = Some(mappings); - any_widget_has_params = true; - } - - let update_payload = CreateWidget { - dashboard_id: server_dashboard_id, - visualization_id: resolve_visualization_id(client, widget, &mut query_cache) - .await?, - text: widget.text.clone(), - width: widget.width, - options, - }; - client.update_widget(widget.id, &update_payload).await?; + client.update_widget(widget.id, &payload).await?; } } diff --git a/src/main.rs b/src/main.rs index 6c9309a..1795e42 100644 --- a/src/main.rs +++ b/src/main.rs @@ -166,52 +166,15 @@ enum DashboardCommands { }, } -#[tokio::main] -async fn main() -> Result<()> { - let version_checker = VersionChecker::new("stmo-cli", env!("CARGO_PKG_VERSION")); - version_checker.check_async(); - - let cli = match Cli::try_parse() { - Ok(cli) => cli, - Err(e) => { - e.print()?; - if e.kind() == clap::error::ErrorKind::DisplayVersion { - version_checker.print_warning_sync(); - } else { - version_checker.print_warning(); - } - std::process::exit(e.exit_code()); - } - }; - - if let Commands::Init = cli.command { - let result = commands::init::init(); - version_checker.print_warning(); - return result; - } - - if let Commands::Update = cli.command { - let result = commands::update::update(); - version_checker.print_warning(); - return result; - } - - let api_key = - std::env::var("REDASH_API_KEY").context("REDASH_API_KEY environment variable not set")?; - - let base_url = std::env::var("REDASH_URL") - .unwrap_or_else(|_| "https://sql.telemetry.mozilla.org".to_string()); - - let client = RedashClient::new(base_url, &api_key)?; - - match cli.command { +async fn run_command(client: RedashClient, command: Commands) -> Result<()> { + match command { Commands::Discover => commands::discover::discover(&client).await?, - Commands::Init => unreachable!("Init handled above"), + Commands::Init | Commands::Update => unreachable!(), Commands::Fetch { query_ids, all } => { - commands::fetch::fetch(&client, query_ids, all).await? + commands::fetch::fetch(&client, query_ids, all).await?; } Commands::Deploy { query_ids, all } => { - commands::deploy::deploy(&client, query_ids, all).await? + commands::deploy::deploy(&client, query_ids, all).await?; } Commands::Execute { query_id, @@ -224,7 +187,6 @@ async fn main() -> Result<()> { let output_format = format .parse::() .context("Invalid output format")?; - let limit_rows = limit; commands::execute::execute( &client, query_id, @@ -232,7 +194,7 @@ async fn main() -> Result<()> { output_format, interactive, timeout, - limit_rows, + limit, ) .await?; } @@ -245,7 +207,6 @@ async fn main() -> Result<()> { let output_format = format .parse::() .context("Invalid output format")?; - if let Some(id) = data_source_id { commands::datasources::show_data_source( &client, @@ -281,21 +242,59 @@ async fn main() -> Result<()> { Commands::Dashboards { command } => match command { DashboardCommands::Discover => commands::dashboards::discover(&client).await?, DashboardCommands::Fetch { slugs } => { - commands::dashboards::fetch(&client, slugs.clone()).await? + commands::dashboards::fetch(&client, slugs).await?; } DashboardCommands::Deploy { slugs, all } => { - commands::dashboards::deploy(&client, slugs.clone(), all).await? + commands::dashboards::deploy(&client, slugs, all).await?; } DashboardCommands::Archive { slugs } => { - commands::dashboards::archive(&client, slugs.clone()).await? + commands::dashboards::archive(&client, slugs).await?; } DashboardCommands::Unarchive { slugs } => { - commands::dashboards::unarchive(&client, slugs.clone()).await? + commands::dashboards::unarchive(&client, slugs).await?; } }, - Commands::Update => unreachable!("Update handled above"), } + Ok(()) +} + +#[tokio::main] +async fn main() -> Result<()> { + let version_checker = VersionChecker::new("stmo-cli", env!("CARGO_PKG_VERSION")); + version_checker.check_async(); + + let cli = match Cli::try_parse() { + Ok(cli) => cli, + Err(e) => { + e.print()?; + if e.kind() == clap::error::ErrorKind::DisplayVersion { + version_checker.print_warning_sync(); + } else { + version_checker.print_warning(); + } + std::process::exit(e.exit_code()); + } + }; + + if let Commands::Init = cli.command { + let result = commands::init::init(); + version_checker.print_warning(); + return result; + } + + if let Commands::Update = cli.command { + let result = commands::update::update(); + version_checker.print_warning(); + return result; + } + + let api_key = + std::env::var("REDASH_API_KEY").context("REDASH_API_KEY environment variable not set")?; + let base_url = std::env::var("REDASH_URL") + .unwrap_or_else(|_| "https://sql.telemetry.mozilla.org".to_string()); + let client = RedashClient::new(base_url, &api_key)?; + run_command(client, cli.command).await?; version_checker.print_warning(); Ok(()) }