From 1651d6ca41eac3feb69bcdc021809c698d464779 Mon Sep 17 00:00:00 2001 From: Zac Farrell Date: Fri, 5 Jun 2026 10:19:51 -0700 Subject: [PATCH 1/2] ci: add cargo fmt check job --- .github/workflows/ci.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1638a77..5145f5f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,19 @@ jobs: git fetch origin "$BASE_REF" python3 scripts/validate-changelog.py "origin/$BASE_REF" + fmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Install Rust + uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable + with: + components: rustfmt + + - name: Check formatting + run: cargo fmt --check + test: runs-on: ubuntu-latest steps: From 734571a0e3b0568522d196fe356260ccdb3f0d4a Mon Sep 17 00:00:00 2001 From: Zac Farrell Date: Fri, 5 Jun 2026 10:19:51 -0700 Subject: [PATCH 2/2] style: apply cargo fmt to codebase --- src/auth.rs | 119 +++++++++++++++++++--------------- src/command.rs | 6 +- src/config.rs | 77 ++++++++++++++++------ src/connections_new.rs | 8 ++- src/context.rs | 5 +- src/database_session.rs | 37 ++++++----- src/databases.rs | 129 ++++++++++++++++++++++++------------- src/datasets.rs | 28 +++++--- src/embedding_providers.rs | 3 +- src/jwt.rs | 100 ++++++++++++++-------------- src/queries.rs | 16 ++--- src/query.rs | 125 ++++++++++++++++++++++++++++------- src/sandbox.rs | 3 +- src/sandbox_session.rs | 40 +++++++----- src/sdk.rs | 1 - src/update.rs | 28 ++++---- src/util.rs | 51 ++++++++++----- src/workspace.rs | 4 +- 18 files changed, 494 insertions(+), 286 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index caefb8d..f2540fb 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -31,33 +31,32 @@ pub fn check_status(profile_config: &config::ProfileConfig) -> AuthStatus { // 2. on-disk sandbox session (sandbox set ) // 3. user-scoped CLI session / api_key fallback let api_url = profile_config.api_url.to_string(); - let access_token = if let Some((sandbox_jwt, _)) = - crate::sandbox_session::sandbox_token_in_use() - { - sandbox_jwt - } else if crate::sandbox_session::load().is_some() { - match crate::sandbox_session::ensure_access_token(&api_url) { - Some(t) => t, - None => return AuthStatus::Invalid(401), - } - } else { - let api_key_fallback = profile_config - .api_key - .as_deref() - .filter(|k| !k.is_empty() && *k != "PLACEHOLDER"); - - // PKCE-origin sessions don't write an api_key, so absence of a key - // alone isn't "not configured" — only true if there's also no - // cached JWT session to validate. - if api_key_fallback.is_none() && crate::jwt::load_session().is_none() { - return AuthStatus::NotConfigured; - } + let access_token = + if let Some((sandbox_jwt, _)) = crate::sandbox_session::sandbox_token_in_use() { + sandbox_jwt + } else if crate::sandbox_session::load().is_some() { + match crate::sandbox_session::ensure_access_token(&api_url) { + Some(t) => t, + None => return AuthStatus::Invalid(401), + } + } else { + let api_key_fallback = profile_config + .api_key + .as_deref() + .filter(|k| !k.is_empty() && *k != "PLACEHOLDER"); - match crate::jwt::ensure_access_token(profile_config, api_key_fallback) { - Ok(t) => t, - Err(_) => return AuthStatus::Invalid(401), - } - }; + // PKCE-origin sessions don't write an api_key, so absence of a key + // alone isn't "not configured" — only true if there's also no + // cached JWT session to validate. + if api_key_fallback.is_none() && crate::jwt::load_session().is_none() { + return AuthStatus::NotConfigured; + } + + match crate::jwt::ensure_access_token(profile_config, api_key_fallback) { + Ok(t) => t, + Err(_) => return AuthStatus::Invalid(401), + } + }; let url = format!("{}/workspaces", profile_config.api_url); let client = reqwest::blocking::Client::new(); @@ -120,8 +119,9 @@ pub fn status(profile: &str) { .api_key .as_deref() .map(crate::util::mask_credential), - ApiKeySource::Config => crate::jwt::load_session() - .map(|s| crate::util::mask_credential(&s.refresh_token)), + ApiKeySource::Config => { + crate::jwt::load_session().map(|s| crate::util::mask_credential(&s.refresh_token)) + } }; (label.to_string(), tail) }; @@ -142,8 +142,20 @@ pub fn status(profile: &str) { ); match profile_config.workspaces.first() { Some(w) => { - print_row("Workspace", &format!("{} {}", w.name.as_str().cyan(), format!("({})", w.public_id).dark_grey())); - print_row("", &"use 'hotdata workspaces set' to switch workspaces".dark_grey().to_string()); + print_row( + "Workspace", + &format!( + "{} {}", + w.name.as_str().cyan(), + format!("({})", w.public_id).dark_grey() + ), + ); + print_row( + "", + &"use 'hotdata workspaces set' to switch workspaces" + .dark_grey() + .to_string(), + ); } None => print_row("Current Workspace", &"None".dark_grey().to_string()), } @@ -163,10 +175,15 @@ pub fn status(profile: &str) { } #[derive(Deserialize)] -struct WsListResponse { workspaces: Vec } +struct WsListResponse { + workspaces: Vec, +} #[derive(Deserialize)] -struct WsItem { public_id: String, name: String } +struct WsItem { + public_id: String, + name: String, +} /// Wait for the browser callback, verify state, and extract the authorization code. /// @@ -179,12 +196,16 @@ fn receive_callback( success_title: &str, success_body: &str, ) -> Result { - let request = server.recv().map_err(|e| format!("failed to receive callback: {e}"))?; + let request = server + .recv() + .map_err(|e| format!("failed to receive callback: {e}"))?; let raw_url = request.url().to_string(); let params = parse_query_params(&raw_url); if params.get("state").map(String::as_str) != Some(expected_state) { - let _ = request.respond(tiny_http::Response::from_string("Login failed: state mismatch")); + let _ = request.respond(tiny_http::Response::from_string( + "Login failed: state mismatch", + )); return Err("state mismatch — possible CSRF attack".into()); } @@ -338,11 +359,17 @@ fn run_browser_auth( Some(w) => { print_row( "Workspace", - &format!("{} {}", w.name.as_str().cyan(), format!("({})", w.public_id).dark_grey()), + &format!( + "{} {}", + w.name.as_str().cyan(), + format!("({})", w.public_id).dark_grey() + ), ); print_row( "", - &"use 'hotdata workspaces set' to switch workspaces".dark_grey().to_string(), + &"use 'hotdata workspaces set' to switch workspaces" + .dark_grey() + .to_string(), ); } None => print_row("Workspace", &"None".dark_grey().to_string()), @@ -619,10 +646,7 @@ mod tests { let (_tmp, _guard) = with_temp_config_dir(); save_test_session("revoked-jwt"); let mut server = mockito::Server::new(); - let mock = server - .mock("GET", "/workspaces") - .with_status(401) - .create(); + let mock = server.mock("GET", "/workspaces").with_status(401).create(); let profile = mock_profile(&server.url(), None); assert_eq!(check_status(&profile), AuthStatus::Invalid(401)); @@ -634,10 +658,7 @@ mod tests { let (_tmp, _guard) = with_temp_config_dir(); save_test_session("jwt"); let mut server = mockito::Server::new(); - let mock = server - .mock("GET", "/workspaces") - .with_status(403) - .create(); + let mock = server.mock("GET", "/workspaces").with_status(403).create(); let profile = mock_profile(&server.url(), None); assert_eq!(check_status(&profile), AuthStatus::Invalid(403)); @@ -651,10 +672,7 @@ mod tests { // the user they need to re-auth. let (_tmp, _guard) = with_temp_config_dir(); let mut server = mockito::Server::new(); - let mock = server - .mock("POST", "/o/token/") - .with_status(401) - .create(); + let mock = server.mock("POST", "/o/token/").with_status(401).create(); let profile = mock_profile(&server.url(), Some("hd_revoked")); assert_eq!(check_status(&profile), AuthStatus::Invalid(401)); @@ -703,10 +721,7 @@ mod tests { let (_tmp, _guard) = with_temp_config_dir(); save_test_session("expired-jwt"); let mut server = mockito::Server::new(); - let mock = server - .mock("GET", "/workspaces") - .with_status(401) - .create(); + let mock = server.mock("GET", "/workspaces").with_status(401).create(); let profile = mock_profile(&server.url(), None); assert!(!is_already_signed_in(&profile)); diff --git a/src/command.rs b/src/command.rs index 5feb3c0..aeff0a2 100644 --- a/src/command.rs +++ b/src/command.rs @@ -469,7 +469,11 @@ pub enum DatasetsCommands { description: Option, /// SQL query to create the dataset from - #[arg(long, conflicts_with = "query_id", required_unless_present = "query_id")] + #[arg( + long, + conflicts_with = "query_id", + required_unless_present = "query_id" + )] sql: Option, /// Saved query ID to create the dataset from diff --git a/src/config.rs b/src/config.rs index 01e77a1..6a4224a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -127,8 +127,8 @@ pub fn clear_workspaces(profile: &str) -> Result<(), String> { return Ok(()); } - let content = fs::read_to_string(&config_path) - .map_err(|e| format!("error reading config file: {e}"))?; + let content = + fs::read_to_string(&config_path).map_err(|e| format!("error reading config file: {e}"))?; let mut config_file: ConfigFile = serde_yaml::from_str(&content).map_err(|e| format!("error parsing config file: {e}"))?; @@ -174,11 +174,15 @@ pub fn save_default_workspace(profile: &str, workspace: WorkspaceEntry) -> Resul .map_err(|e| format!("error reading config file: {e}"))?; serde_yaml::from_str(&content).map_err(|e| format!("error parsing config file: {e}"))? } else { - ConfigFile { profiles: HashMap::new() } + ConfigFile { + profiles: HashMap::new(), + } }; let entry = config_file.profiles.entry(profile.to_string()).or_default(); - entry.workspaces.retain(|w| w.public_id != workspace.public_id); + entry + .workspaces + .retain(|w| w.public_id != workspace.public_id); entry.workspaces.insert(0, workspace); let content = serde_yaml::to_string(&config_file) @@ -194,7 +198,9 @@ pub fn save_sandbox(profile: &str, sandbox_id: &str) -> Result<(), String> { .map_err(|e| format!("error reading config file: {e}"))?; serde_yaml::from_str(&content).map_err(|e| format!("error parsing config file: {e}"))? } else { - ConfigFile { profiles: HashMap::new() } + ConfigFile { + profiles: HashMap::new(), + } }; config_file @@ -215,8 +221,8 @@ pub fn clear_sandbox(profile: &str) -> Result<(), String> { return Ok(()); } - let content = fs::read_to_string(&config_path) - .map_err(|e| format!("error reading config file: {e}"))?; + let content = + fs::read_to_string(&config_path).map_err(|e| format!("error reading config file: {e}"))?; let mut config_file: ConfigFile = serde_yaml::from_str(&content).map_err(|e| format!("error parsing config file: {e}"))?; @@ -229,7 +235,11 @@ pub fn clear_sandbox(profile: &str) -> Result<(), String> { write_config(&config_path, &content) } -pub fn save_current_database(profile: &str, workspace_id: &str, database_id: &str) -> Result<(), String> { +pub fn save_current_database( + profile: &str, + workspace_id: &str, + database_id: &str, +) -> Result<(), String> { let config_path = config_path()?; let mut config_file: ConfigFile = if config_path.exists() { @@ -237,7 +247,9 @@ pub fn save_current_database(profile: &str, workspace_id: &str, database_id: &st .map_err(|e| format!("error reading config file: {e}"))?; serde_yaml::from_str(&content).map_err(|e| format!("error parsing config file: {e}"))? } else { - ConfigFile { profiles: HashMap::new() } + ConfigFile { + profiles: HashMap::new(), + } }; config_file @@ -259,7 +271,12 @@ pub fn load_current_database(profile: &str, workspace_id: &str) -> Option Result<(), String> { @@ -269,8 +286,8 @@ pub fn clear_current_database(profile: &str, workspace_id: &str) -> Result<(), S return Ok(()); } - let content = fs::read_to_string(&config_path) - .map_err(|e| format!("error reading config file: {e}"))?; + let content = + fs::read_to_string(&config_path).map_err(|e| format!("error reading config file: {e}"))?; let mut config_file: ConfigFile = serde_yaml::from_str(&content).map_err(|e| format!("error parsing config file: {e}"))?; @@ -283,7 +300,10 @@ pub fn clear_current_database(profile: &str, workspace_id: &str) -> Result<(), S write_config(&config_path, &content) } -pub fn resolve_workspace_id(provided: Option, profile_config: &ProfileConfig) -> Result { +pub fn resolve_workspace_id( + provided: Option, + profile_config: &ProfileConfig, +) -> Result { if let Some(id) = provided { return Ok(id); } @@ -306,14 +326,20 @@ pub fn load(profile: &str) -> Result { let config_file = config_path()?; let mut profile_config = if config_file.exists() { - let content = - fs::read_to_string(&config_file).map_err(|e| format!("error reading config file: {e}"))?; + let content = fs::read_to_string(&config_file) + .map_err(|e| format!("error reading config file: {e}"))?; let config_file: ConfigFile = serde_yaml::from_str(&content).unwrap_or_else(|_| { eprintln!("{}", "error parsing config file.".red()); - eprintln!("Run 'hotdata auth login' (or 'hotdata auth') to generate a new config file."); + eprintln!( + "Run 'hotdata auth login' (or 'hotdata auth') to generate a new config file." + ); std::process::exit(1); }); - config_file.profiles.get(profile).cloned().unwrap_or_default() + config_file + .profiles + .get(profile) + .cloned() + .unwrap_or_default() } else { ProfileConfig::default() }; @@ -364,11 +390,14 @@ pub mod test_helpers { #[cfg(test)] mod tests { - use super::*; use super::test_helpers::with_temp_config_dir; + use super::*; fn ws(id: &str, name: &str) -> WorkspaceEntry { - WorkspaceEntry { public_id: id.into(), name: name.into() } + WorkspaceEntry { + public_id: id.into(), + name: name.into(), + } } #[test] @@ -490,7 +519,10 @@ mod tests { #[test] fn resolve_workspace_id_prefers_provided() { let profile = ProfileConfig { - workspaces: vec![WorkspaceEntry { public_id: "ws-1".into(), name: "WS".into() }], + workspaces: vec![WorkspaceEntry { + public_id: "ws-1".into(), + name: "WS".into(), + }], ..Default::default() }; let result = resolve_workspace_id(Some("explicit-id".into()), &profile).unwrap(); @@ -500,7 +532,10 @@ mod tests { #[test] fn resolve_workspace_id_falls_back_to_first() { let profile = ProfileConfig { - workspaces: vec![WorkspaceEntry { public_id: "ws-1".into(), name: "WS".into() }], + workspaces: vec![WorkspaceEntry { + public_id: "ws-1".into(), + name: "WS".into(), + }], ..Default::default() }; let result = resolve_workspace_id(None, &profile).unwrap(); diff --git a/src/connections_new.rs b/src/connections_new.rs index 985545c..be9465b 100644 --- a/src/connections_new.rs +++ b/src/connections_new.rs @@ -2,7 +2,7 @@ use inquire::validator::Validation; use inquire::{Confirm, Password, Select, Text}; use serde_json::{Map, Number, Value}; -use crate::sdk::{block, Api, ApiError}; +use crate::sdk::{Api, ApiError, block}; // ── SDK helpers ───────────────────────────────────────────────────────────── @@ -372,7 +372,11 @@ pub fn run(workspace_id: &str) { format!("{} {}", "healthy".green(), format!("({ms}ms)").dark_grey()) } HealthStatus::Available(h) => { - let err = h.error.as_ref().and_then(|e| e.as_deref()).unwrap_or("unknown error"); + let err = h + .error + .as_ref() + .and_then(|e| e.as_deref()) + .unwrap_or("unknown error"); format!("{} — {}", "unhealthy".red(), err) } HealthStatus::Unavailable(err) => { diff --git a/src/context.rs b/src/context.rs index 5afc055..7a4c035 100644 --- a/src/context.rs +++ b/src/context.rs @@ -295,9 +295,8 @@ pub fn push(workspace_id: &str, database_id: &str, name: &str, dry_run: bool) { let api = Api::new(Some(workspace_id)); let request = UpsertDatabaseContextRequest::new(content, name.clone()); - let resp = match crate::sdk::block( - api.client().database_context().upsert(database_id, request), - ) { + let resp = match crate::sdk::block(api.client().database_context().upsert(database_id, request)) + { Ok(resp) => resp, Err(ApiError::Status { status: _, body }) => { let msg = crate::util::api_error(body); diff --git a/src/database_session.rs b/src/database_session.rs index aba7ab4..01557d4 100644 --- a/src/database_session.rs +++ b/src/database_session.rs @@ -30,7 +30,9 @@ pub struct DatabaseSession { } pub fn session_path() -> Option { - config::config_dir().ok().map(|d| d.join("database_session.json")) + config::config_dir() + .ok() + .map(|d| d.join("database_session.json")) } #[allow(dead_code)] // Reserved for flows that re-use a cached database session. @@ -45,8 +47,8 @@ pub fn save(session: &DatabaseSession) -> Result<(), String> { if let Some(parent) = path.parent() { fs::create_dir_all(parent).map_err(|e| format!("mkdir failed: {e}"))?; } - let json = serde_json::to_string_pretty(session) - .map_err(|e| format!("serialize failed: {e}"))?; + let json = + serde_json::to_string_pretty(session).map_err(|e| format!("serialize failed: {e}"))?; use std::os::unix::fs::OpenOptionsExt; let mut f = fs::OpenOptions::new() @@ -104,18 +106,16 @@ pub fn refresh(api_url: &str, refresh_token: &str) -> Result Result(&format!("/databases/{encoded}"), &[])) - .unwrap_or_else(|e| e.exit()) + if let Some(db) = none_if_404(api.get_json::(&format!("/databases/{encoded}"), &[])) + .unwrap_or_else(|e| e.exit()) { return Ok(db); } @@ -129,7 +128,6 @@ pub fn try_resolve_database(api: &Api, id_or_name: &str) -> Result = body .databases .iter() @@ -174,10 +172,7 @@ pub fn create_database_request( let mut req = serde_json::Map::new(); if let Some(n) = name { - req.insert( - "name".to_string(), - serde_json::Value::String(n.to_string()), - ); + req.insert("name".to_string(), serde_json::Value::String(n.to_string())); } if let Some(c) = catalog { @@ -212,7 +207,10 @@ pub fn create_database_request( }) }) .collect(); - req.insert("schemas".to_string(), serde_json::Value::Array(schemas_json)); + req.insert( + "schemas".to_string(), + serde_json::Value::Array(schemas_json), + ); } if let Some(exp) = expires_at { @@ -430,18 +428,13 @@ fn collect_tables(api: &Api, connection_id: &str, schema: Option<&str>) -> Vec println!("{}", serde_json::to_string_pretty(&body.databases).unwrap()), @@ -460,7 +453,11 @@ pub fn list(workspace_id: &str, format: &str) { .databases .iter() .map(|d| { - let marker = if current.as_deref() == Some(d.id.as_str()) { "*" } else { "" }; + let marker = if current.as_deref() == Some(d.id.as_str()) { + "*" + } else { + "" + }; vec![ marker.to_string(), d.id.clone(), @@ -497,13 +494,18 @@ pub fn get(workspace_id: &str, id_or_name: &str, format: &str) { label("default_connection_id:"), db.default_connection_id.clone().dark_cyan() ); - let catalog = db.default_catalog.as_deref() + let catalog = db + .default_catalog + .as_deref() .or(db.name.as_deref()) .unwrap_or("default"); println!( "{}{}", label("sql_prefix:"), - format!("{catalog}.{{schema}}.{{table}} (pass X-Database-Id header when querying)").green() + format!( + "{catalog}.{{schema}}.{{table}} (pass X-Database-Id header when querying)" + ) + .green() ); if !db.attachments.is_empty() { println!("{}({})", label("attached catalogs:"), db.attachments.len()); @@ -537,7 +539,9 @@ fn create_and_return_id( ) -> String { use crossterm::style::Stylize; let body = create_database_request(name, None, schema, tables, expires_at); - let (status, resp_body) = api.post_raw("/databases", &body).unwrap_or_else(|e| e.exit()); + let (status, resp_body) = api + .post_raw("/databases", &body) + .unwrap_or_else(|e| e.exit()); if !status.is_success() { eprintln!("{}", crate::util::api_error(resp_body).red()); std::process::exit(1); @@ -561,8 +565,9 @@ fn mint_database_token(api: &Api, database_id: &str) -> DatabaseTokenResponse { "grant_type": "existing_database", "database_id": database_id, }); - let (status, resp_body) = - api.post_raw("/auth/database", &body).unwrap_or_else(|e| e.exit()); + let (status, resp_body) = api + .post_raw("/auth/database", &body) + .unwrap_or_else(|e| e.exit()); if !status.is_success() { // The old typed `api.post` routed non-success through `fail_response`, // which upgrades a masked 401/403/404 into the re-auth hint. Reproduce @@ -612,7 +617,10 @@ pub fn run( let db_jwt = resp.token.clone(); let db_refresh = resp.refresh_token.clone(); - let now = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0); let session = crate::database_session::DatabaseSession { access_token: db_jwt.clone(), refresh_token: db_refresh.clone(), @@ -686,7 +694,10 @@ pub fn create( if let Err(e) = crate::config::save_current_database("default", workspace_id, &result.id) { use crossterm::style::Stylize; - eprintln!("{}", format!("warning: database created but could not set as current: {e}").yellow()); + eprintln!( + "{}", + format!("warning: database created but could not set as current: {e}").yellow() + ); } match format { @@ -705,7 +716,9 @@ pub fn create( println!("expires_at: {exp}"); } println!(); - let catalog = result.default_catalog.as_deref() + let catalog = result + .default_catalog + .as_deref() .or(result.name.as_deref()) .unwrap_or("default"); println!( @@ -799,7 +812,11 @@ pub fn tables_list(workspace_id: &str, database: Option<&str>, schema: Option<&s let database = resolve_current_database(database, workspace_id); let api = Api::new(Some(workspace_id)); let db = resolve_database(&api, &database); - let catalog = db.default_catalog.as_deref().or(db.name.as_deref()).unwrap_or("default"); + let catalog = db + .default_catalog + .as_deref() + .or(db.name.as_deref()) + .unwrap_or("default"); let tables = collect_tables(&api, &db.default_connection_id, schema); let rows = table_rows(catalog, tables); @@ -916,7 +933,11 @@ pub fn tables_load( .collect(); if !synced.is_empty() { use crossterm::style::Stylize; - let catalog = db.default_catalog.as_deref().or(db.name.as_deref()).unwrap_or(&db.id); + let catalog = db + .default_catalog + .as_deref() + .or(db.name.as_deref()) + .unwrap_or(&db.id); eprintln!( "{}", format!( @@ -962,8 +983,9 @@ pub fn tables_load( &all_tables, db.expires_at.as_deref(), ); - let (create_status, create_body_resp) = - api.post_raw("/databases", &create_body).unwrap_or_else(|e| e.exit()); + let (create_status, create_body_resp) = api + .post_raw("/databases", &create_body) + .unwrap_or_else(|e| e.exit()); if !create_status.is_success() { eprintln!("{}", crate::util::api_error(create_body_resp).red()); std::process::exit(1); @@ -1001,7 +1023,11 @@ pub fn tables_load( } }; - let catalog = db.default_catalog.as_deref().or(db.name.as_deref()).unwrap_or("default"); + let catalog = db + .default_catalog + .as_deref() + .or(db.name.as_deref()) + .unwrap_or("default"); let full_name = format!("{catalog}.{}.{}", result.schema_name, result.table_name); println!("{}", "Table loaded".green()); println!("full_name: {}", full_name.clone().green()); @@ -1022,7 +1048,12 @@ pub fn tables_load( ); } -pub fn tables_delete(workspace_id: &str, database: Option<&str>, table: &str, schema: Option<&str>) { +pub fn tables_delete( + workspace_id: &str, + database: Option<&str>, + table: &str, + schema: Option<&str>, +) { use crossterm::style::Stylize; let database = resolve_current_database(database, workspace_id); @@ -1038,7 +1069,11 @@ pub fn tables_delete(workspace_id: &str, database: Option<&str>, table: &str, sc std::process::exit(1); } - let catalog = db.default_catalog.as_deref().or(db.name.as_deref()).unwrap_or("default"); + let catalog = db + .default_catalog + .as_deref() + .or(db.name.as_deref()) + .unwrap_or("default"); println!( "{}", format!("Table '{catalog}.{schema}.{table}' deleted.").green() @@ -1242,13 +1277,16 @@ mod tests { #[test] fn table_rows_uses_default_prefix() { - let rows = table_rows("default", vec![InfoTable { - connection: "ignored".into(), - schema: "public".into(), - table: "orders".into(), - synced: true, - last_sync: Some("2026-05-19T00:00:00Z".into()), - }]); + let rows = table_rows( + "default", + vec![InfoTable { + connection: "ignored".into(), + schema: "public".into(), + table: "orders".into(), + synced: true, + last_sync: Some("2026-05-19T00:00:00Z".into()), + }], + ); assert_eq!(rows.len(), 1); assert_eq!(rows[0].full_name, "default.public.orders"); assert!(rows[0].synced); @@ -1298,9 +1336,7 @@ mod tests { .mock("POST", "/v1/databases") .match_header("X-Workspace-Id", "ws-test") .with_status(201) - .with_body( - r#"{"id":"db_new","name":"mydb","default_connection_id":"conn_abc"}"#, - ) + .with_body(r#"{"id":"db_new","name":"mydb","default_connection_id":"conn_abc"}"#) .match_body(mockito::Matcher::JsonString( serde_json::to_string(&create_database_request( Some("mydb"), @@ -1314,7 +1350,8 @@ mod tests { .create(); let api = Api::test_new(&server.url(), "k", Some("ws-test")); - let body = create_database_request(Some("mydb"), None, "public", &["gdp".to_string()], None); + let body = + create_database_request(Some("mydb"), None, "public", &["gdp".to_string()], None); let (status, resp_body) = api.post_raw("/databases", &body).unwrap(); assert_eq!(status.as_u16(), 201); let parsed: CreateDatabaseResponse = serde_json::from_str(&resp_body).unwrap(); @@ -1430,7 +1467,9 @@ mod tests { ))) .with_status(201) .with_header("content-type", "application/json") - .with_body(r#"{"id":"dbid_new","description":"scratch","default_connection_id":"conn_1"}"#) + .with_body( + r#"{"id":"dbid_new","description":"scratch","default_connection_id":"conn_1"}"#, + ) .create(); let api = Api::test_new(&server.url(), "k", Some("ws")); let id = create_and_return_id(&api, Some("scratch"), "public", &[], None); diff --git a/src/datasets.rs b/src/datasets.rs index e858fd6..801532f 100644 --- a/src/datasets.rs +++ b/src/datasets.rs @@ -91,14 +91,18 @@ impl From for UpdateView { } } -fn create_dataset(api: &Api, description: Option<&str>, name: &str, source: DatasetSource, format: &str) { +fn create_dataset( + api: &Api, + description: Option<&str>, + name: &str, + source: DatasetSource, + format: &str, +) { let label = description.unwrap_or(name).to_string(); let mut request = CreateDatasetRequest::new(label, source); request.table_name = Some(Some(name.to_string())); - let resp = match crate::sdk::block( - api.client().datasets().create(request, api.database_id()), - ) { + let resp = match crate::sdk::block(api.client().datasets().create(request, api.database_id())) { Ok(r) => r, Err(e) => e.exit(), }; @@ -121,7 +125,13 @@ fn create_dataset(api: &Api, description: Option<&str>, name: &str, source: Data } } -pub fn create_from_query(workspace_id: &str, sql: &str, description: Option<&str>, name: &str, format: &str) { +pub fn create_from_query( + workspace_id: &str, + sql: &str, + description: Option<&str>, + name: &str, + format: &str, +) { let api = Api::new(Some(workspace_id)); let source = DatasetSource::DatasetSourceOneOf2(Box::new(DatasetSourceOneOf2::new( sql.to_string(), @@ -331,8 +341,8 @@ pub fn refresh(workspace_id: &str, dataset_id: &str, async_mode: bool) { request.r#async = Some(true); } - let resp = crate::sdk::block(api.client().refresh().refresh(request)) - .unwrap_or_else(|e| e.exit()); + let resp = + crate::sdk::block(api.client().refresh().refresh(request)).unwrap_or_else(|e| e.exit()); if async_mode { let job_id = match &resp { @@ -352,9 +362,7 @@ pub fn refresh(workspace_id: &str, dataset_id: &str, async_mode: bool) { RefreshResponse::RefreshDatasetResponse(r) => { (r.id.clone(), r.version as i64, r.status.clone()) } - RefreshResponse::SubmitJobResponse(j) => { - (j.id.clone(), 0, j.status.to_string()) - } + RefreshResponse::SubmitJobResponse(j) => (j.id.clone(), 0, j.status.to_string()), _ => ("unknown".to_string(), 0, String::new()), }; println!("{}", "Dataset refresh completed.".green()); diff --git a/src/embedding_providers.rs b/src/embedding_providers.rs index 5d17645..2e99525 100644 --- a/src/embedding_providers.rs +++ b/src/embedding_providers.rs @@ -159,7 +159,8 @@ pub fn update( if name.is_none() && config.is_none() && api_key.is_none() && secret_name.is_none() { eprintln!( "{}", - "error: provide at least one of --name, --config, --provider-api-key, --secret-name.".red() + "error: provide at least one of --name, --config, --provider-api-key, --secret-name." + .red() ); std::process::exit(1); } diff --git a/src/jwt.rs b/src/jwt.rs index a6c6aa9..c9e3b60 100644 --- a/src/jwt.rs +++ b/src/jwt.rs @@ -64,7 +64,8 @@ pub fn save_session(session: &Session) -> Result<(), String> { if let Some(parent) = path.parent() { fs::create_dir_all(parent).map_err(|e| format!("mkdir failed: {e}"))?; } - let json = serde_json::to_string_pretty(session).map_err(|e| format!("serialize failed: {e}"))?; + let json = + serde_json::to_string_pretty(session).map_err(|e| format!("serialize failed: {e}"))?; // mode 0600 — session file contains a refresh token, treat it like a // credential on disk. @@ -101,7 +102,11 @@ struct TokenResponse { refresh_token: Option, } -fn session_from_response(resp: TokenResponse, fallback_refresh: Option, source: &str) -> Session { +fn session_from_response( + resp: TokenResponse, + fallback_refresh: Option, + source: &str, +) -> Session { let refresh_token = resp.refresh_token.or(fallback_refresh).unwrap_or_default(); // We don't know the exact refresh TTL server-side (7 d or 36 h // depending on origin). Store a conservative estimate so we don't @@ -125,7 +130,11 @@ fn oauth_base(profile: &config::ProfileConfig) -> String { // DOT (`/o/authorize/`, `/o/token/`, …) is mounted on the webapp // (app_url), not the API. The api_url host typically only serves // the `/v1` runtimedb routes. - profile.app_url.to_string().trim_end_matches('/').to_string() + profile + .app_url + .to_string() + .trim_end_matches('/') + .to_string() } /// Build a redacted JSON view of a form body for `--debug` printing. @@ -173,13 +182,9 @@ pub fn exchange_cli_register_code( let client = reqwest::blocking::Client::new(); let req = client.post(&url).json(&body); - let (status, body_text) = util::send_debug_with_redaction( - &client, - req, - Some(&body_log), - &["token"], - ) - .map_err(|e| format!("connection error: {e}"))?; + let (status, body_text) = + util::send_debug_with_redaction(&client, req, Some(&body_log), &["token"]) + .map_err(|e| format!("connection error: {e}"))?; if !status.is_success() { return Err(format!( "registration token exchange failed: HTTP {status}: {body_text}" @@ -190,8 +195,8 @@ pub fn exchange_cli_register_code( struct RegisterResponse { token: String, } - let resp: RegisterResponse = serde_json::from_str(&body_text) - .map_err(|e| format!("malformed token response: {e}"))?; + let resp: RegisterResponse = + serde_json::from_str(&body_text).map_err(|e| format!("malformed token response: {e}"))?; mint_from_api_token(profile, &resp.token) } @@ -215,18 +220,14 @@ pub fn mint_from_pkce_code( let client = reqwest::blocking::Client::new(); let req = client.post(&url).form(¶ms); let body_log = redacted_form_body(¶ms); - let (status, body_text) = util::send_debug_with_redaction( - &client, - req, - Some(&body_log), - TOKEN_REDACT_KEYS, - ) - .map_err(|e| format!("connection error: {e}"))?; + let (status, body_text) = + util::send_debug_with_redaction(&client, req, Some(&body_log), TOKEN_REDACT_KEYS) + .map_err(|e| format!("connection error: {e}"))?; if !status.is_success() { return Err(format!("token exchange failed: HTTP {status}: {body_text}")); } - let body: TokenResponse = serde_json::from_str(&body_text) - .map_err(|e| format!("malformed token response: {e}"))?; + let body: TokenResponse = + serde_json::from_str(&body_text).map_err(|e| format!("malformed token response: {e}"))?; Ok(session_from_response(body, None, "pkce")) } @@ -245,18 +246,16 @@ pub fn mint_from_api_token( let client = reqwest::blocking::Client::new(); let req = client.post(&url).form(¶ms); let body_log = redacted_form_body(¶ms); - let (status, body_text) = util::send_debug_with_redaction( - &client, - req, - Some(&body_log), - TOKEN_REDACT_KEYS, - ) - .map_err(|e| format!("connection error: {e}"))?; + let (status, body_text) = + util::send_debug_with_redaction(&client, req, Some(&body_log), TOKEN_REDACT_KEYS) + .map_err(|e| format!("connection error: {e}"))?; if !status.is_success() { - return Err(format!("api_token exchange failed: HTTP {status}: {body_text}")); + return Err(format!( + "api_token exchange failed: HTTP {status}: {body_text}" + )); } - let body: TokenResponse = serde_json::from_str(&body_text) - .map_err(|e| format!("malformed token response: {e}"))?; + let body: TokenResponse = + serde_json::from_str(&body_text).map_err(|e| format!("malformed token response: {e}"))?; Ok(session_from_response(body, None, "api_token")) } @@ -272,18 +271,14 @@ pub fn refresh(profile: &config::ProfileConfig, session: &Session) -> Result Result { match mode { - AuthMode::DatabaseEnv { api_url } => { - crate::database_session::refresh_from_env(api_url) - .ok_or_else(|| "HOTDATA_DATABASE_TOKEN is empty".to_string()) - } - AuthMode::SandboxEnv { api_url } => { - crate::sandbox_session::refresh_from_env(api_url) - .ok_or_else(|| "HOTDATA_SANDBOX_TOKEN is empty".to_string()) - } + AuthMode::DatabaseEnv { api_url } => crate::database_session::refresh_from_env(api_url) + .ok_or_else(|| "HOTDATA_DATABASE_TOKEN is empty".to_string()), + AuthMode::SandboxEnv { api_url } => crate::sandbox_session::refresh_from_env(api_url) + .ok_or_else(|| "HOTDATA_SANDBOX_TOKEN is empty".to_string()), AuthMode::SandboxSession { api_url } => { crate::sandbox_session::ensure_access_token(api_url) .ok_or_else(|| "sandbox session expired".to_string()) @@ -512,7 +505,10 @@ mod tests { save_session(&Session::default()).unwrap(); let path = session_path().unwrap(); let mode = fs::metadata(&path).unwrap().permissions().mode() & 0o777; - assert_eq!(mode, 0o600, "session file must be 0600 (contains refresh token)"); + assert_eq!( + mode, 0o600, + "session file must be 0600 (contains refresh token)" + ); } #[test] @@ -786,9 +782,7 @@ mod tests { .mock("POST", "/o/token/") .with_status(200) .with_header("content-type", "application/json") - .with_body( - r#"{"access_token":"new-jwt","expires_in":300,"refresh_token":"rotated"}"#, - ) + .with_body(r#"{"access_token":"new-jwt","expires_in":300,"refresh_token":"rotated"}"#) .create(); let profile = mock_profile(&server.url()); diff --git a/src/queries.rs b/src/queries.rs index 8f6fb49..42c05b6 100644 --- a/src/queries.rs +++ b/src/queries.rs @@ -192,10 +192,7 @@ pub fn list( let next_cursor = resp.next_cursor.flatten(); match format { - "json" => println!( - "{}", - serde_json::to_string_pretty(&query_runs).unwrap() - ), + "json" => println!("{}", serde_json::to_string_pretty(&query_runs).unwrap()), "yaml" => print!("{}", serde_yaml::to_string(&query_runs).unwrap()), "table" => { if query_runs.is_empty() { @@ -228,10 +225,7 @@ pub fn list( let next = next_cursor.as_deref().unwrap_or(""); eprintln!( "{}", - format!( - "showing {count} results — use --cursor {next} for more" - ) - .dark_grey() + format!("showing {count} results — use --cursor {next} for more").dark_grey() ); } } @@ -254,7 +248,11 @@ fn print_detail(r: &QueryRun, format: &str) { "table" => { let label = |l: &str| format!("{:<14}", l).dark_grey().to_string(); println!("{}{}", label("id:"), r.id); - println!("{}{}", label("status:"), crate::util::color_status(&r.status)); + println!( + "{}{}", + label("status:"), + crate::util::color_status(&r.status) + ); println!( "{}{}", label("created:"), diff --git a/src/query.rs b/src/query.rs index f95f57a..2402119 100644 --- a/src/query.rs +++ b/src/query.rs @@ -55,28 +55,101 @@ fn arrow_cell(col: &dyn arrow::array::Array, row: usize) -> Value { } match col.data_type() { - Boolean => Value::Bool(col.as_any().downcast_ref::().unwrap().value(row)), - Int8 => Value::Number(col.as_any().downcast_ref::().unwrap().value(row).into()), - Int16 => Value::Number(col.as_any().downcast_ref::().unwrap().value(row).into()), - Int32 => Value::Number(col.as_any().downcast_ref::().unwrap().value(row).into()), - Int64 => Value::Number(col.as_any().downcast_ref::().unwrap().value(row).into()), - UInt8 => Value::Number(col.as_any().downcast_ref::().unwrap().value(row).into()), - UInt16 => Value::Number(col.as_any().downcast_ref::().unwrap().value(row).into()), - UInt32 => Value::Number(col.as_any().downcast_ref::().unwrap().value(row).into()), - UInt64 => Value::Number(col.as_any().downcast_ref::().unwrap().value(row).into()), + Boolean => Value::Bool( + col.as_any() + .downcast_ref::() + .unwrap() + .value(row), + ), + Int8 => Value::Number( + col.as_any() + .downcast_ref::() + .unwrap() + .value(row) + .into(), + ), + Int16 => Value::Number( + col.as_any() + .downcast_ref::() + .unwrap() + .value(row) + .into(), + ), + Int32 => Value::Number( + col.as_any() + .downcast_ref::() + .unwrap() + .value(row) + .into(), + ), + Int64 => Value::Number( + col.as_any() + .downcast_ref::() + .unwrap() + .value(row) + .into(), + ), + UInt8 => Value::Number( + col.as_any() + .downcast_ref::() + .unwrap() + .value(row) + .into(), + ), + UInt16 => Value::Number( + col.as_any() + .downcast_ref::() + .unwrap() + .value(row) + .into(), + ), + UInt32 => Value::Number( + col.as_any() + .downcast_ref::() + .unwrap() + .value(row) + .into(), + ), + UInt64 => Value::Number( + col.as_any() + .downcast_ref::() + .unwrap() + .value(row) + .into(), + ), Float32 => { - let v = col.as_any().downcast_ref::().unwrap().value(row) as f64; - Number::from_f64(v).map(Value::Number).unwrap_or(Value::Null) + let v = col + .as_any() + .downcast_ref::() + .unwrap() + .value(row) as f64; + Number::from_f64(v) + .map(Value::Number) + .unwrap_or(Value::Null) } Float64 => { - let v = col.as_any().downcast_ref::().unwrap().value(row); - Number::from_f64(v).map(Value::Number).unwrap_or(Value::Null) + let v = col + .as_any() + .downcast_ref::() + .unwrap() + .value(row); + Number::from_f64(v) + .map(Value::Number) + .unwrap_or(Value::Null) } Utf8 => Value::String( - col.as_any().downcast_ref::().unwrap().value(row).to_owned(), + col.as_any() + .downcast_ref::() + .unwrap() + .value(row) + .to_owned(), ), LargeUtf8 => Value::String( - col.as_any().downcast_ref::().unwrap().value(row).to_owned(), + col.as_any() + .downcast_ref::() + .unwrap() + .value(row) + .to_owned(), ), // Dates, timestamps, decimals, etc. — format via Arrow's display helper. _ => { @@ -102,7 +175,12 @@ fn arrow_ipc_to_query_response(bytes: Vec, result_id: String) -> QueryRespon } }; - let columns: Vec = reader.schema().fields().iter().map(|f| f.name().clone()).collect(); + let columns: Vec = reader + .schema() + .fields() + .iter() + .map(|f| f.name().clone()) + .collect(); let mut rows: Vec> = Vec::new(); for batch_result in reader { @@ -114,7 +192,11 @@ fn arrow_ipc_to_query_response(bytes: Vec, result_id: String) -> QueryRespon } }; for row in 0..batch.num_rows() { - rows.push((0..batch.num_columns()).map(|c| arrow_cell(batch.column(c).as_ref(), row)).collect()); + rows.push( + (0..batch.num_columns()) + .map(|c| arrow_cell(batch.column(c).as_ref(), row)) + .collect(), + ); } } @@ -198,8 +280,8 @@ pub fn execute( loop { // Drive the poll loop ourselves to preserve the 5-minute deadline and // 500ms cadence (NOT the SDK's PollConfig defaults). - let run = crate::sdk::block(api.client().query_runs().get(run_id)) - .unwrap_or_else(|e| e.exit()); + let run = + crate::sdk::block(api.client().query_runs().get(run_id)).unwrap_or_else(|e| e.exit()); match run.status.as_str() { "succeeded" => { spinner.finish_and_clear(); @@ -255,8 +337,8 @@ pub fn execute( pub fn poll(query_run_id: &str, workspace_id: &str, format: &str) { let api = Api::new(Some(workspace_id)); - let run = crate::sdk::block(api.client().query_runs().get(query_run_id)) - .unwrap_or_else(|e| e.exit()); + let run = + crate::sdk::block(api.client().query_runs().get(query_run_id)).unwrap_or_else(|e| e.exit()); match run.status.as_str() { "succeeded" => match run.result_id.flatten() { @@ -351,4 +433,3 @@ pub fn print_result(result: &QueryResponse, format: &str) { _ => unreachable!(), } } - diff --git a/src/sandbox.rs b/src/sandbox.rs index b9606e8..dad2a71 100644 --- a/src/sandbox.rs +++ b/src/sandbox.rs @@ -1,6 +1,6 @@ use crate::config; use crate::sandbox_session::{self, SandboxSession}; -use crate::sdk::{block, Api, ApiError}; +use crate::sdk::{Api, ApiError, block}; use crossterm::style::Stylize; use hotdata::models::UpdateSandboxRequest; use serde::Deserialize; @@ -382,5 +382,4 @@ mod tests { find_sandbox_run_ancestor_inner() ); } - } diff --git a/src/sandbox_session.rs b/src/sandbox_session.rs index 3a4c0df..e0ca3bb 100644 --- a/src/sandbox_session.rs +++ b/src/sandbox_session.rs @@ -37,7 +37,9 @@ pub struct SandboxSession { } pub fn session_path() -> Option { - config::config_dir().ok().map(|d| d.join("sandbox_session.json")) + config::config_dir() + .ok() + .map(|d| d.join("sandbox_session.json")) } #[allow(dead_code)] // Reserved for parent-side flows that resurrect a session. @@ -52,8 +54,8 @@ pub fn save(session: &SandboxSession) -> Result<(), String> { if let Some(parent) = path.parent() { fs::create_dir_all(parent).map_err(|e| format!("mkdir failed: {e}"))?; } - let json = serde_json::to_string_pretty(session) - .map_err(|e| format!("serialize failed: {e}"))?; + let json = + serde_json::to_string_pretty(session).map_err(|e| format!("serialize failed: {e}"))?; use std::os::unix::fs::OpenOptionsExt; let mut f = fs::OpenOptions::new() @@ -113,19 +115,20 @@ pub fn refresh(api_url: &str, refresh_token: &str) -> Result Option { let session = load()?; let now = now_unix(); - if !session.access_token.is_empty() && now + REFRESH_LEEWAY_SECONDS < session.access_expires_at { + if !session.access_token.is_empty() && now + REFRESH_LEEWAY_SECONDS < session.access_expires_at + { return Some(session.access_token); } @@ -286,7 +290,11 @@ mod tests { use std::os::unix::fs::PermissionsExt; let (_tmp, _guard) = with_temp_config_dir(); save(&mk_session(60, 60)).unwrap(); - let mode = fs::metadata(session_path().unwrap()).unwrap().permissions().mode() & 0o777; + let mode = fs::metadata(session_path().unwrap()) + .unwrap() + .permissions() + .mode() + & 0o777; assert_eq!(mode, 0o600); } diff --git a/src/sdk.rs b/src/sdk.rs index 03ce01a..bf695a8 100644 --- a/src/sdk.rs +++ b/src/sdk.rs @@ -464,7 +464,6 @@ impl Api { } } - pub fn workspace_id(&self) -> Option<&str> { self.workspace_id.as_deref() } diff --git a/src/update.rs b/src/update.rs index dfbda84..f3b9dac 100644 --- a/src/update.rs +++ b/src/update.rs @@ -43,7 +43,9 @@ struct UpdateCheckCache { } fn cache_path() -> Option { - crate::config::config_dir().ok().map(|d| d.join(".update_check.json")) + crate::config::config_dir() + .ok() + .map(|d| d.join(".update_check.json")) } fn now_secs() -> u64 { @@ -76,7 +78,10 @@ fn fetch_latest_version() -> Result { .map_err(|e| e.to_string())?; let resp = client .get(&url) - .header("User-Agent", concat!("hotdata-cli/", env!("CARGO_PKG_VERSION"))) + .header( + "User-Agent", + concat!("hotdata-cli/", env!("CARGO_PKG_VERSION")), + ) .header("Accept", "application/vnd.github+json") .send() .map_err(|e| e.to_string())?; @@ -183,10 +188,7 @@ fn run_homebrew_upgrade() { .copied(); let Some(brew_bin) = brew else { - eprintln!( - "{}", - "brew not found — run manually:".yellow() - ); + eprintln!("{}", "brew not found — run manually:".yellow()); println!(" {}", format!("brew upgrade {HOMEBREW_FORMULA}").cyan()); return; }; @@ -206,10 +208,7 @@ fn run_homebrew_upgrade() { } } Ok(s) => { - eprintln!( - "{}", - format!("brew upgrade exited with status {s}").red() - ); + eprintln!("{}", format!("brew upgrade exited with status {s}").red()); std::process::exit(s.code().unwrap_or(1)); } Err(e) => { @@ -241,7 +240,10 @@ pub fn run_update() { let latest = match fetch_latest_version() { Ok(v) => v, Err(e) => { - eprintln!("{}", format!("error: could not check for updates: {e}").red()); + eprintln!( + "{}", + format!("error: could not check for updates: {e}").red() + ); std::process::exit(1); } }; @@ -302,9 +304,7 @@ fn perform_update(version: &Version) -> Result<(), String> { if !resp.status().is_success() { return Err(format!("HTTP {} downloading {asset_name}", resp.status())); } - let xz_bytes = resp - .bytes() - .map_err(|e| format!("reading download: {e}"))?; + let xz_bytes = resp.bytes().map_err(|e| format!("reading download: {e}"))?; let mut tar_bytes: Vec = Vec::with_capacity(xz_bytes.len() * 4); lzma_rs::xz_decompress(&mut std::io::Cursor::new(&xz_bytes[..]), &mut tar_bytes) diff --git a/src/util.rs b/src/util.rs index e50ecab..d3846dc 100644 --- a/src/util.rs +++ b/src/util.rs @@ -5,9 +5,7 @@ use std::time::Duration; /// Writes to stderr so stdout (json/yaml output) stays clean. pub fn spinner(msg: &str) -> indicatif::ProgressBar { let pb = indicatif::ProgressBar::new_spinner(); - pb.set_style( - indicatif::ProgressStyle::with_template("{spinner:.cyan} {msg}").unwrap(), - ); + pb.set_style(indicatif::ProgressStyle::with_template("{spinner:.cyan} {msg}").unwrap()); pb.set_message(msg.to_string()); pb.enable_steady_tick(Duration::from_millis(80)); pb @@ -45,15 +43,25 @@ pub fn is_debug() -> bool { } /// Log request details when debug mode is enabled. -pub fn debug_request(method: &str, url: &str, headers: &[(&str, &str)], body: Option<&serde_json::Value>) { - if !is_debug() { return; } +pub fn debug_request( + method: &str, + url: &str, + headers: &[(&str, &str)], + body: Option<&serde_json::Value>, +) { + if !is_debug() { + return; + } use crossterm::style::Stylize; eprintln!("{}", format!(">>> {method} {url}").dark_cyan()); for (k, v) in headers { eprintln!("{}", format!(" {k}: {v}").dark_grey()); } if let Some(b) = body { - eprintln!("{}", colorize_json(&serde_json::to_string_pretty(b).unwrap())); + eprintln!( + "{}", + colorize_json(&serde_json::to_string_pretty(b).unwrap()) + ); } } @@ -86,7 +94,10 @@ pub fn debug_response_redacted( if !redact_keys.is_empty() { redact_json_fields(&mut v, redact_keys); } - eprintln!("{}", colorize_json(&serde_json::to_string_pretty(&v).unwrap())); + eprintln!( + "{}", + colorize_json(&serde_json::to_string_pretty(&v).unwrap()) + ); } else if !body.is_empty() { eprintln!("{}", body.to_string().dark_grey()); } @@ -148,10 +159,7 @@ pub fn send_debug_with_redaction( Ok(debug_response_redacted(resp, response_redact_keys)) } -fn log_request_struct( - req: &reqwest::blocking::Request, - body: Option<&serde_json::Value>, -) { +fn log_request_struct(req: &reqwest::blocking::Request, body: Option<&serde_json::Value>) { let method = req.method().as_str(); let url = req.url().as_str(); // Materialize masked header pairs as owned strings, then re-borrow @@ -237,8 +245,10 @@ fn colorize_json(json: &str) -> String { // String value in array result.push_str(&line.yellow().to_string()); } - } else if trimmed.starts_with('{') || trimmed.starts_with('}') - || trimmed.starts_with('[') || trimmed.starts_with(']') + } else if trimmed.starts_with('{') + || trimmed.starts_with('}') + || trimmed.starts_with('[') + || trimmed.starts_with(']') { result.push_str(&line.dark_grey().to_string()); } else { @@ -260,11 +270,16 @@ fn colorize_json(json: &str) -> String { /// Find the colon separating a JSON key from its value, skipping the key string. fn find_key_colon(s: &str) -> Option { // Expect: "key": value - if !s.starts_with('"') { return None; } + if !s.starts_with('"') { + return None; + } let mut i = 1; let bytes = s.as_bytes(); while i < bytes.len() { - if bytes[i] == b'\\' { i += 2; continue; } + if bytes[i] == b'\\' { + i += 2; + continue; + } if bytes[i] == b'"' { // Found end of key, look for ": " if s.get(i + 1..i + 3) == Some(": ") { @@ -281,7 +296,11 @@ fn find_key_colon(s: &str) -> Option { fn colorize_json_value(v: &str) -> String { use crossterm::style::Stylize; let stripped = v.trim_end_matches(','); - let comma = if v.ends_with(',') { ",".dark_grey().to_string() } else { String::new() }; + let comma = if v.ends_with(',') { + ",".dark_grey().to_string() + } else { + String::new() + }; let colored = if stripped == "null" { stripped.dark_grey().to_string() diff --git a/src/workspace.rs b/src/workspace.rs index ee30fe8..e09060b 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -25,9 +25,7 @@ impl From<&hotdata::models::WorkspaceListItem> for Workspace { fn fetch_workspaces() -> Vec { let api = Api::new(None); - let body = api - .list_workspaces(None) - .unwrap_or_else(|e| e.exit()); + let body = api.list_workspaces(None).unwrap_or_else(|e| e.exit()); body.workspaces.iter().map(Workspace::from).collect() }