From 0aef201d50b3115f2a140effd63c2faf9fc2ff6a Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 27 Jul 2026 20:39:21 +1000 Subject: [PATCH] bonsaiviewer-autodesk: satisfy cargo fmt and clippy The crate had never been run through rustfmt, so the CI job's first step (`cargo fmt --all -- --check`) failed and masked 12 clippy errors behind it. Fix both. Beyond the mechanical reformat and the redundant-closure/div_ceil/Default lints, two changes carry meaning: - SettingsDialog::on_reload is UI-thread only, so it becomes an Rc. The Arc was never shared across threads and clippy rightly flagged it as an Arc over a non-Send/Sync closure. - WorkerMsg variants lose their shared `Loaded` postfix; the enum's doc comment already says these are worker completions. Co-Authored-By: Claude Opus 5 --- src/bonsaiviewer-autodesk/src/aps/browse.rs | 78 ++++- src/bonsaiviewer-autodesk/src/aps/mod.rs | 4 +- src/bonsaiviewer-autodesk/src/aps/transfer.rs | 153 ++++++-- src/bonsaiviewer-autodesk/src/auth.rs | 81 +++-- src/bonsaiviewer-autodesk/src/cache.rs | 8 +- src/bonsaiviewer-autodesk/src/connector.rs | 327 +++++++++++++----- src/bonsaiviewer-autodesk/src/progress.rs | 43 ++- src/bonsaiviewer-autodesk/src/rpc.rs | 86 ++++- src/bonsaiviewer-autodesk/src/ui/browse.rs | 104 ++++-- src/bonsaiviewer-autodesk/src/ui/settings.rs | 14 +- src/bonsaiviewer-autodesk/tests/aps.rs | 92 +++-- src/bonsaiviewer-autodesk/tests/rpc_smoke.rs | 18 +- 12 files changed, 750 insertions(+), 258 deletions(-) diff --git a/src/bonsaiviewer-autodesk/src/aps/browse.rs b/src/bonsaiviewer-autodesk/src/aps/browse.rs index 564392eecc..be0dd79a77 100644 --- a/src/bonsaiviewer-autodesk/src/aps/browse.rs +++ b/src/bonsaiviewer-autodesk/src/aps/browse.rs @@ -77,7 +77,11 @@ impl ApsClient { .and_then(|v| v.as_array()) .map(|arr| arr.iter().map(entry_from_jsonapi).collect()) .unwrap_or_default(); - folders.sort_by(|a, b| a.display_name.to_lowercase().cmp(&b.display_name.to_lowercase())); + folders.sort_by(|a, b| { + a.display_name + .to_lowercase() + .cmp(&b.display_name.to_lowercase()) + }); Ok(folders) } @@ -120,7 +124,11 @@ impl ApsClient { _ => break, } } - entries.sort_by(|a, b| a.display_name.to_lowercase().cmp(&b.display_name.to_lowercase())); + entries.sort_by(|a, b| { + a.display_name + .to_lowercase() + .cmp(&b.display_name.to_lowercase()) + }); Ok(entries) } @@ -136,10 +144,17 @@ impl ApsClient { .get("data") .cloned() .ok_or_else(|| RpcError::internal("Item response missing 'data'."))?; - let item_attrs = item.get("attributes").cloned().unwrap_or_else(|| serde_json::json!({})); + let item_attrs = item + .get("attributes") + .cloned() + .unwrap_or_else(|| serde_json::json!({})); let parent_folder_id = relationship_id(&item, "parent"); let tip_id = relationship_id(&item, "tip"); - let id = item.get("id").and_then(|v| v.as_str()).unwrap_or_default().to_string(); + let id = item + .get("id") + .and_then(|v| v.as_str()) + .unwrap_or_default() + .to_string(); let tip = payload .get("included") .and_then(|v| v.as_array()) @@ -171,7 +186,10 @@ impl ApsClient { parent_folder_id, }); }; - let tip_attrs = tip.get("attributes").cloned().unwrap_or_else(|| serde_json::json!({})); + let tip_attrs = tip + .get("attributes") + .cloned() + .unwrap_or_else(|| serde_json::json!({})); let display = tip_attrs .get("displayName") .and_then(|v| v.as_str()) @@ -182,12 +200,21 @@ impl ApsClient { Ok(ItemTip { id, display_name: display, - hidden: item_attrs.get("hidden").and_then(|v| v.as_bool()).unwrap_or(false), + hidden: item_attrs + .get("hidden") + .and_then(|v| v.as_bool()) + .unwrap_or(false), version_id: tip.get("id").and_then(|v| v.as_str()).map(String::from), storage_id: relationship_id(&tip, "storage"), version_number: tip_attrs.get("versionNumber").cloned(), - last_modified_time_utc: tip_attrs.get("lastModifiedTime").and_then(|v| v.as_str()).map(String::from), - last_modified_user_name: tip_attrs.get("lastModifiedUserName").and_then(|v| v.as_str()).map(String::from), + last_modified_time_utc: tip_attrs + .get("lastModifiedTime") + .and_then(|v| v.as_str()) + .map(String::from), + last_modified_user_name: tip_attrs + .get("lastModifiedUserName") + .and_then(|v| v.as_str()) + .map(String::from), parent_folder_id, }) } @@ -195,8 +222,16 @@ impl ApsClient { fn hub_from_jsonapi(item: &Value) -> Hub { Hub { - id: item.get("id").and_then(|v| v.as_str()).unwrap_or_default().to_string(), - name: item.pointer("/attributes/name").and_then(|v| v.as_str()).unwrap_or_default().to_string(), + id: item + .get("id") + .and_then(|v| v.as_str()) + .unwrap_or_default() + .to_string(), + name: item + .pointer("/attributes/name") + .and_then(|v| v.as_str()) + .unwrap_or_default() + .to_string(), extension_type: item .pointer("/attributes/extension/type") .and_then(|v| v.as_str()) @@ -207,8 +242,16 @@ fn hub_from_jsonapi(item: &Value) -> Hub { fn project_from_jsonapi(item: &Value) -> Project { Project { - id: item.get("id").and_then(|v| v.as_str()).unwrap_or_default().to_string(), - name: item.pointer("/attributes/name").and_then(|v| v.as_str()).unwrap_or_default().to_string(), + id: item + .get("id") + .and_then(|v| v.as_str()) + .unwrap_or_default() + .to_string(), + name: item + .pointer("/attributes/name") + .and_then(|v| v.as_str()) + .unwrap_or_default() + .to_string(), extension_type: item .pointer("/attributes/extension/type") .and_then(|v| v.as_str()) @@ -223,7 +266,10 @@ fn project_from_jsonapi(item: &Value) -> Project { } pub(crate) fn entry_from_jsonapi(item: &Value) -> Entry { - let attrs = item.get("attributes").cloned().unwrap_or_else(|| serde_json::json!({})); + let attrs = item + .get("attributes") + .cloned() + .unwrap_or_else(|| serde_json::json!({})); let display = attrs .get("displayName") .and_then(|v| v.as_str()) @@ -237,7 +283,11 @@ pub(crate) fn entry_from_jsonapi(item: &Value) -> Entry { _ => EntryType::Other, }; Entry { - id: item.get("id").and_then(|v| v.as_str()).unwrap_or_default().to_string(), + id: item + .get("id") + .and_then(|v| v.as_str()) + .unwrap_or_default() + .to_string(), entry_type, display_name: display, name: attrs.get("name").and_then(|v| v.as_str()).map(String::from), diff --git a/src/bonsaiviewer-autodesk/src/aps/mod.rs b/src/bonsaiviewer-autodesk/src/aps/mod.rs index 9831d8260e..1869b8d888 100644 --- a/src/bonsaiviewer-autodesk/src/aps/mod.rs +++ b/src/bonsaiviewer-autodesk/src/aps/mod.rs @@ -23,7 +23,9 @@ impl ApsClient { pub fn with_base_url(auth: Arc, base_url: String) -> Self { Self { auth, - agent: ureq::AgentBuilder::new().timeout(Duration::from_secs(120)).build(), + agent: ureq::AgentBuilder::new() + .timeout(Duration::from_secs(120)) + .build(), base_url, } } diff --git a/src/bonsaiviewer-autodesk/src/aps/transfer.rs b/src/bonsaiviewer-autodesk/src/aps/transfer.rs index 7f9a1d3797..d9c32398cb 100644 --- a/src/bonsaiviewer-autodesk/src/aps/transfer.rs +++ b/src/bonsaiviewer-autodesk/src/aps/transfer.rs @@ -43,9 +43,13 @@ impl ApsClient { local_path.display() ))); } - let file_name = display_name - .map(str::to_string) - .unwrap_or_else(|| local_path.file_name().unwrap_or_default().to_string_lossy().into_owned()); + let file_name = display_name.map(str::to_string).unwrap_or_else(|| { + local_path + .file_name() + .unwrap_or_default() + .to_string_lossy() + .into_owned() + }); let storage_id = self.create_storage(project_id, folder_id, &file_name)?; let (bucket, object) = parse_storage_id(&storage_id)?; self.upload_local_file_to_oss(&bucket, &object, local_path, progress)?; @@ -68,7 +72,9 @@ impl ApsClient { .and_then(|v| v.as_str()) .filter(|s| !s.is_empty()) .map(String::from) - .ok_or_else(|| RpcError::internal("Signed download URL response did not contain a URL.")) + .ok_or_else(|| { + RpcError::internal("Signed download URL response did not contain a URL.") + }) } fn download_to_file( @@ -81,7 +87,11 @@ impl ApsClient { Ok(r) => r, Err(ureq::Error::Status(code, resp)) => { let body = resp.into_string().unwrap_or_default(); - let msg = if body.trim().is_empty() { format!("HTTP {code}") } else { body.trim().to_string() }; + let msg = if body.trim().is_empty() { + format!("HTTP {code}") + } else { + body.trim().to_string() + }; return Err(RpcError::internal(msg)); } Err(e) => return Err(RpcError::internal(e.to_string())), @@ -93,8 +103,9 @@ impl ApsClient { .to_string_lossy() .into_owned(); let mut reader = resp.into_reader(); - let mut file = File::create(destination) - .map_err(|e| RpcError::internal(format!("Cannot create '{}': {e}", destination.display())))?; + let mut file = File::create(destination).map_err(|e| { + RpcError::internal(format!("Cannot create '{}': {e}", destination.display())) + })?; let mut buf = vec![0u8; READ_BUF]; let mut downloaded: u64 = 0; loop { @@ -108,16 +119,26 @@ impl ApsClient { .map_err(|e| RpcError::internal(format!("Download write failed: {e}")))?; downloaded += n as u64; if let Some(cb) = &progress { - let percent = total_bytes.map(|t| min(100, ((downloaded as f64 / t as f64) * 100.0) as i32)); + let percent = + total_bytes.map(|t| min(100, ((downloaded as f64 / t as f64) * 100.0) as i32)); cb(&name, percent, Some(downloaded), total_bytes); } } Ok(()) } - fn create_storage(&self, project_id: &str, folder_id: &str, file_name: &str) -> Result { + fn create_storage( + &self, + project_id: &str, + folder_id: &str, + file_name: &str, + ) -> Result { let payload = self.post_json( - &format!("{}/data/v1/projects/{}/storage", self.base_url, url_enc(project_id)), + &format!( + "{}/data/v1/projects/{}/storage", + self.base_url, + url_enc(project_id) + ), &json!({ "jsonapi": {"version": "1.0"}, "data": { @@ -145,7 +166,7 @@ impl ApsClient { let file_size = std::fs::metadata(local_path) .map_err(|e| RpcError::internal(format!("stat '{}': {e}", local_path.display())))? .len(); - let total_parts = ((file_size + CHUNK_SIZE - 1) / CHUNK_SIZE).max(1); + let total_parts = file_size.div_ceil(CHUNK_SIZE).max(1); let file_name = local_path .file_name() .unwrap_or_default() @@ -170,13 +191,18 @@ impl ApsClient { parts_to_request, )?; if upload_key.is_none() { - upload_key = signed.get("uploadKey").and_then(|v| v.as_str()).map(String::from); + upload_key = signed + .get("uploadKey") + .and_then(|v| v.as_str()) + .map(String::from); } let urls = signed .get("urls") .and_then(|v| v.as_array()) .filter(|a| !a.is_empty()) - .ok_or_else(|| RpcError::internal("Upload URL response did not contain upload URLs."))? + .ok_or_else(|| { + RpcError::internal("Upload URL response did not contain upload URLs.") + })? .clone(); for url in &urls { if parts_uploaded >= total_parts { @@ -197,14 +223,23 @@ impl ApsClient { let percent = if file_size == 0 { 100 } else { - min(100, ((bytes_uploaded as f64 / file_size as f64) * 100.0) as i32) + min( + 100, + ((bytes_uploaded as f64 / file_size as f64) * 100.0) as i32, + ) }; - cb(&file_name, Some(percent), Some(bytes_uploaded), Some(file_size)); + cb( + &file_name, + Some(percent), + Some(bytes_uploaded), + Some(file_size), + ); } } } - let key = upload_key.ok_or_else(|| RpcError::internal("Upload did not return an upload key."))?; + let key = + upload_key.ok_or_else(|| RpcError::internal("Upload did not return an upload key."))?; self.complete_signed_upload(bucket, object, &key) } @@ -216,7 +251,9 @@ impl ApsClient { first_part: u32, parts: u32, ) -> Result { - let token = self.auth.ensure_access_token(crate::progress::noop_auth())?; + let token = self + .auth + .ensure_access_token(crate::progress::noop_auth())?; let mut url = format!( "{}/oss/v2/buckets/{}/objects/{}/signeds3upload?minutesExpiration=10&firstPart={}&parts={}", self.base_url, @@ -236,8 +273,15 @@ impl ApsClient { decode_json(resp, &url) } - fn complete_signed_upload(&self, bucket: &str, object: &str, upload_key: &str) -> Result<(), RpcError> { - let token = self.auth.ensure_access_token(crate::progress::noop_auth())?; + fn complete_signed_upload( + &self, + bucket: &str, + object: &str, + upload_key: &str, + ) -> Result<(), RpcError> { + let token = self + .auth + .ensure_access_token(crate::progress::noop_auth())?; let url = format!( "{}/oss/v2/buckets/{}/objects/{}/signeds3upload", self.base_url, @@ -254,7 +298,11 @@ impl ApsClient { Ok(_) => Ok(()), Err(ureq::Error::Status(code, resp)) => { let body = resp.into_string().unwrap_or_default(); - let msg = if body.trim().is_empty() { format!("HTTP {code}") } else { body.trim().to_string() }; + let msg = if body.trim().is_empty() { + format!("HTTP {code}") + } else { + body.trim().to_string() + }; Err(RpcError::internal(msg)) } Err(e) => Err(RpcError::internal(e.to_string())), @@ -271,7 +319,11 @@ impl ApsClient { Ok(_) => Ok(()), Err(ureq::Error::Status(code, resp)) => { let body = resp.into_string().unwrap_or_default(); - let msg = if body.trim().is_empty() { format!("HTTP {code}") } else { body.trim().to_string() }; + let msg = if body.trim().is_empty() { + format!("HTTP {code}") + } else { + body.trim().to_string() + }; Err(RpcError::internal(msg)) } Err(e) => Err(RpcError::internal(e.to_string())), @@ -299,7 +351,11 @@ impl ApsClient { storage_id: &str, ) -> Result { let payload = self.post_json( - &format!("{}/data/v1/projects/{}/versions", self.base_url, url_enc(project_id)), + &format!( + "{}/data/v1/projects/{}/versions", + self.base_url, + url_enc(project_id) + ), &json!({ "jsonapi": {"version": "1.0"}, "data": { @@ -318,14 +374,27 @@ impl ApsClient { let version = payload .get("data") .ok_or_else(|| RpcError::internal("Create version response missing 'data'."))?; - let attrs = version.get("attributes").cloned().unwrap_or_else(|| json!({})); + let attrs = version + .get("attributes") + .cloned() + .unwrap_or_else(|| json!({})); Ok(UploadResult { item_id: item_id.to_string(), - version_id: version.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string(), + version_id: version + .get("id") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(), display_name: file_name.to_string(), version_number: attrs.get("versionNumber").cloned(), - last_modified_time_utc: attrs.get("lastModifiedTime").and_then(|v| v.as_str()).map(String::from), - last_modified_user_name: attrs.get("lastModifiedUserName").and_then(|v| v.as_str()).map(String::from), + last_modified_time_utc: attrs + .get("lastModifiedTime") + .and_then(|v| v.as_str()) + .map(String::from), + last_modified_user_name: attrs + .get("lastModifiedUserName") + .and_then(|v| v.as_str()) + .map(String::from), }) } @@ -337,7 +406,11 @@ impl ApsClient { storage_id: &str, ) -> Result { let payload = self.post_json( - &format!("{}/data/v1/projects/{}/items", self.base_url, url_enc(project_id)), + &format!( + "{}/data/v1/projects/{}/items", + self.base_url, + url_enc(project_id) + ), &json!({ "jsonapi": {"version": "1.0"}, "data": { @@ -386,8 +459,14 @@ impl ApsClient { if let Some(v) = attrs.get("versionNumber") { version_number = Some(v.clone()); } - last_modified_time = attrs.get("lastModifiedTime").and_then(|v| v.as_str()).map(String::from); - last_modified_user = attrs.get("lastModifiedUserName").and_then(|v| v.as_str()).map(String::from); + last_modified_time = attrs + .get("lastModifiedTime") + .and_then(|v| v.as_str()) + .map(String::from); + last_modified_user = attrs + .get("lastModifiedUserName") + .and_then(|v| v.as_str()) + .map(String::from); } } } @@ -404,14 +483,16 @@ impl ApsClient { pub(crate) fn parse_storage_id(storage_id: &str) -> Result<(String, String), RpcError> { let marker = "urn:adsk.objects:os.object:"; - let path = storage_id - .strip_prefix(marker) - .ok_or_else(|| RpcError::internal(format!("Unsupported storage identifier '{storage_id}'.")))?; - let slash = path - .find('/') - .ok_or_else(|| RpcError::internal(format!("Malformed storage identifier '{storage_id}'.")))?; + let path = storage_id.strip_prefix(marker).ok_or_else(|| { + RpcError::internal(format!("Unsupported storage identifier '{storage_id}'.")) + })?; + let slash = path.find('/').ok_or_else(|| { + RpcError::internal(format!("Malformed storage identifier '{storage_id}'.")) + })?; if slash == 0 || slash == path.len() - 1 { - return Err(RpcError::internal(format!("Malformed storage identifier '{storage_id}'."))); + return Err(RpcError::internal(format!( + "Malformed storage identifier '{storage_id}'." + ))); } Ok((path[..slash].to_string(), path[slash + 1..].to_string())) } diff --git a/src/bonsaiviewer-autodesk/src/auth.rs b/src/bonsaiviewer-autodesk/src/auth.rs index e8fdf0d518..c56a2a6b64 100644 --- a/src/bonsaiviewer-autodesk/src/auth.rs +++ b/src/bonsaiviewer-autodesk/src/auth.rs @@ -16,7 +16,8 @@ use crate::progress::AuthProgress; use crate::rpc::RpcError; pub const KEYRING_SERVICE: &str = "bonsaiviewer-autodesk"; -pub const AUTHORIZE_ENDPOINT: &str = "https://developer.api.autodesk.com/authentication/v2/authorize"; +pub const AUTHORIZE_ENDPOINT: &str = + "https://developer.api.autodesk.com/authentication/v2/authorize"; pub const TOKEN_ENDPOINT: &str = "https://developer.api.autodesk.com/authentication/v2/token"; const REFRESH_TTL_DEFAULT_SECONDS: i64 = 15 * 24 * 60 * 60; @@ -44,7 +45,10 @@ pub struct KeyringTokenStore { impl KeyringTokenStore { pub fn new(username: impl Into) -> Self { - Self { service: KEYRING_SERVICE.to_string(), username: username.into() } + Self { + service: KEYRING_SERVICE.to_string(), + username: username.into(), + } } fn entry(&self) -> Result { @@ -128,8 +132,9 @@ pub fn wait_for_oauth_callback( path: &str, expected_state: &str, ) -> Result { - let listener = TcpListener::bind((host, port)) - .map_err(|e| RpcError::internal(format!("Cannot bind OAuth callback to {host}:{port}: {e}")))?; + let listener = TcpListener::bind((host, port)).map_err(|e| { + RpcError::internal(format!("Cannot bind OAuth callback to {host}:{port}: {e}")) + })?; let (mut stream, _) = listener .accept() .map_err(|e| RpcError::internal(format!("OAuth callback accept failed: {e}")))?; @@ -137,7 +142,9 @@ pub fn wait_for_oauth_callback( let request_line = { let mut reader = BufReader::new( - stream.try_clone().map_err(|e| RpcError::internal(e.to_string()))?, + stream + .try_clone() + .map_err(|e| RpcError::internal(e.to_string()))?, ); let mut line = String::new(); reader @@ -184,7 +191,9 @@ pub fn wait_for_oauth_callback( } if let Some(err) = error.filter(|s| !s.is_empty()) { - return Err(RpcError::internal(format!("Autodesk returned OAuth error '{err}'."))); + return Err(RpcError::internal(format!( + "Autodesk returned OAuth error '{err}'." + ))); } if state.as_deref() != Some(expected_state) { return Err(RpcError::internal("OAuth state mismatch.")); @@ -195,10 +204,11 @@ pub fn wait_for_oauth_callback( /// Pluggable callback strategy — production binds a localhost socket, tests /// inject a stub that returns a canned code. -pub type CallbackWaiter = Arc Result + Send + Sync>; +pub type CallbackWaiter = + Arc Result + Send + Sync>; pub fn default_callback_waiter() -> CallbackWaiter { - Arc::new(|host, port, path, state| wait_for_oauth_callback(host, port, path, state)) + Arc::new(wait_for_oauth_callback) } /// Pluggable "open this URL in a browser" — production opens it for real, @@ -235,7 +245,12 @@ pub struct AuthBuilder { } impl AuthBuilder { - pub fn new(client_id: String, callback_url: String, scope: String, token_store: Box) -> Self { + pub fn new( + client_id: String, + callback_url: String, + scope: String, + token_store: Box, + ) -> Self { Self { client_id, callback_url, @@ -317,7 +332,9 @@ impl AuthSessionService { if callback.scheme() != "http" || !matches!(callback.host_str(), Some("127.0.0.1") | Some("localhost")) { - return Err(RpcError::internal("Callback URL must be http://localhost or http://127.0.0.1.")); + return Err(RpcError::internal( + "Callback URL must be http://localhost or http://127.0.0.1.", + )); } let authorize_url = { @@ -356,7 +373,11 @@ impl AuthSessionService { Ok(token) } - fn refresh(&self, token: &StoredToken, progress: AuthProgress) -> Result { + fn refresh( + &self, + token: &StoredToken, + progress: AuthProgress, + ) -> Result { progress("auth", "Refreshing Autodesk session", None); let payload = self .post_form(&[ @@ -408,7 +429,8 @@ impl AuthSessionService { access_token: access_token.to_string(), refresh_token: refresh_token.to_string(), access_token_expires_at: now + ChronoDuration::seconds(expires_in - TOKEN_SKEW_SECONDS), - refresh_token_expires_at: now + ChronoDuration::seconds(refresh_ttl - TOKEN_SKEW_SECONDS), + refresh_token_expires_at: now + + ChronoDuration::seconds(refresh_ttl - TOKEN_SKEW_SECONDS), scope: self.scope.clone(), }) } @@ -421,10 +443,14 @@ pub struct InMemoryTokenStore { impl InMemoryTokenStore { pub fn empty() -> Self { - Self { inner: std::sync::Mutex::new(None) } + Self { + inner: std::sync::Mutex::new(None), + } } pub fn preloaded(token: StoredToken) -> Self { - Self { inner: std::sync::Mutex::new(Some(token)) } + Self { + inner: std::sync::Mutex::new(Some(token)), + } } } @@ -483,7 +509,11 @@ mod tests { assert!(store.load().unwrap().is_none()); } - fn make_service(store: Box, token_endpoint: String, code: &'static str) -> AuthSessionService { + fn make_service( + store: Box, + token_endpoint: String, + code: &'static str, + ) -> AuthSessionService { AuthBuilder::new( "client-xyz".into(), "http://127.0.0.1:8080/".into(), @@ -539,15 +569,22 @@ mod tests { inner: InMemoryTokenStore, } impl TokenStore for PeekStore { - fn load(&self) -> Result, RpcError> { self.inner.load() } + fn load(&self) -> Result, RpcError> { + self.inner.load() + } fn save(&self, t: &StoredToken) -> Result<(), RpcError> { *self.shadow.lock().unwrap() = Some(t.clone()); self.inner.save(t) } - fn delete(&self) -> Result<(), RpcError> { self.inner.delete() } + fn delete(&self) -> Result<(), RpcError> { + self.inner.delete() + } } let _ = store; - let peek = Box::new(PeekStore { shadow: saved_check.clone(), inner: InMemoryTokenStore::empty() }); + let peek = Box::new(PeekStore { + shadow: saved_check.clone(), + inner: InMemoryTokenStore::empty(), + }); let svc = make_service(peek, endpoint, "AUTH_CODE"); let token = svc.login_interactive(crate::progress::noop_auth()).unwrap(); @@ -570,7 +607,9 @@ mod tests { }; let store = Box::new(InMemoryTokenStore::preloaded(token)); let svc = make_service(store, "http://127.0.0.1:1/never".into(), "x"); - let access = svc.ensure_access_token(crate::progress::noop_auth()).unwrap(); + let access = svc + .ensure_access_token(crate::progress::noop_auth()) + .unwrap(); assert_eq!(access, "CACHED"); } @@ -591,7 +630,9 @@ mod tests { }; let store = Box::new(InMemoryTokenStore::preloaded(token)); let svc = make_service(store, endpoint, "unused"); - let access = svc.ensure_access_token(crate::progress::noop_auth()).unwrap(); + let access = svc + .ensure_access_token(crate::progress::noop_auth()) + .unwrap(); assert_eq!(access, "FRESH"); } } diff --git a/src/bonsaiviewer-autodesk/src/cache.rs b/src/bonsaiviewer-autodesk/src/cache.rs index 1cae588a98..edcf0057b0 100644 --- a/src/bonsaiviewer-autodesk/src/cache.rs +++ b/src/bonsaiviewer-autodesk/src/cache.rs @@ -46,13 +46,17 @@ fn hash_parts(parts: &[&str]) -> String { /// Stable directory for an .ifcfed; re-downloads overwrite in place so the /// viewer's open path remains valid across sync operations. pub fn ifcfed_dir(project_id: &str, item_id: &str) -> PathBuf { - cache_root().join("ifcfeds").join(hash_parts(&[project_id, item_id])) + cache_root() + .join("ifcfeds") + .join(hash_parts(&[project_id, item_id])) } /// Per-version directory for a model. A new resolved version → a new /// directory, so sidecars regenerate when the model file changes. pub fn model_dir(project_id: &str, item_id: &str, version_id: &str) -> PathBuf { - cache_root().join("models").join(hash_parts(&[project_id, item_id, version_id])) + cache_root() + .join("models") + .join(hash_parts(&[project_id, item_id, version_id])) } /// Clear the directory so the file we write is the only child. diff --git a/src/bonsaiviewer-autodesk/src/connector.rs b/src/bonsaiviewer-autodesk/src/connector.rs index 4e9872f1e9..743c8fb663 100644 --- a/src/bonsaiviewer-autodesk/src/connector.rs +++ b/src/bonsaiviewer-autodesk/src/connector.rs @@ -83,9 +83,17 @@ pub struct AutodeskConnector { inner: RefCell>, } +impl Default for AutodeskConnector { + fn default() -> Self { + Self::new() + } +} + impl AutodeskConnector { pub fn new() -> Self { - let me = Self { inner: RefCell::new(None) }; + let me = Self { + inner: RefCell::new(None), + }; me.reload_credentials(); me } @@ -144,7 +152,7 @@ impl AutodeskConnector { // SAFETY: SettingsDialog::run blocks the calling thread, and the // callback fires only on this thread before run returns. The pointer // outlives the call because `self` is borrowed by the RPC dispatch. - let on_reload: Arc Result<(), RpcError>> = Arc::new(move || { + let on_reload: Rc Result<(), RpcError>> = Rc::new(move || { let cell: &RefCell> = unsafe { &*inner_ptr }; let client_id = cfg::load_client_id(); if client_id.is_empty() { @@ -181,7 +189,14 @@ impl AutodeskConnector { let display_name = entry.display_name.clone(); let aps = pair.aps.clone(); let path = run_with_progress("Downloading project", move |report| { - download_ifcfed(&aps, &hub_id, &project_id, &item_id, &display_name, Some(download_callback(report, 0, 0))) + download_ifcfed( + &aps, + &hub_id, + &project_id, + &item_id, + &display_name, + Some(download_callback(report, 0, 0)), + ) })?; Ok(json!({"path": path.to_string_lossy()})) } @@ -190,13 +205,19 @@ impl AutodeskConnector { fn pull_ifcfed(&self, params: Value) -> Result { let pair = self.require()?; - let p: PullIfcfedParams = serde_json::from_value(params).map_err(|e| { - RpcError::invalid_params(format!("pull_ifcfed: {e}")) - })?; + let p: PullIfcfedParams = serde_json::from_value(params) + .map_err(|e| RpcError::invalid_params(format!("pull_ifcfed: {e}")))?; let display = p.display_name.unwrap_or_else(|| p.item_id.clone()); let aps = pair.aps.clone(); let path = run_with_progress("Downloading project", move |report| { - download_ifcfed(&aps, &p.hub_id, &p.project_id, &p.item_id, &display, Some(download_callback(report, 0, 0))) + download_ifcfed( + &aps, + &p.hub_id, + &p.project_id, + &p.item_id, + &display, + Some(download_callback(report, 0, 0)), + ) })?; Ok(json!({"path": path.to_string_lossy()})) } @@ -205,9 +226,8 @@ impl AutodeskConnector { fn pull_models(&self, params: Value) -> Result { let pair = self.require()?; - let entries: Vec = serde_json::from_value(params).map_err(|e| { - RpcError::invalid_params(format!("pull_models: {e}")) - })?; + let entries: Vec = serde_json::from_value(params) + .map_err(|e| RpcError::invalid_params(format!("pull_models: {e}")))?; let total = entries.len(); let aps = pair.aps.clone(); let results = run_with_progress("Downloading models", move |report| { @@ -241,7 +261,14 @@ impl AutodeskConnector { let mut out: Vec = Vec::new(); for (index, entry) in chosen.entries.into_iter().enumerate() { let cb = download_callback(report.clone(), index + 1, total); - match download_picked_model(&aps, &hub.id, &project.id, &entry.id, &entry.display_name, Some(cb)) { + match download_picked_model( + &aps, + &hub.id, + &project.id, + &entry.id, + &entry.display_name, + Some(cb), + ) { Ok(Some(v)) => out.push(v), Ok(None) => {} Err(e) => eprintln!("pull_models_interactive[{index}] skipped: {}", e.message), @@ -256,16 +283,18 @@ impl AutodeskConnector { fn push_ifcfed_interactive(&self, params: Value) -> Result { let pair = self.require()?; - let p: PushInteractiveParams = serde_json::from_value(params).map_err(|e| { - RpcError::invalid_params(format!("push_ifcfed_interactive: {e}")) - })?; + let p: PushInteractiveParams = serde_json::from_value(params) + .map_err(|e| RpcError::invalid_params(format!("push_ifcfed_interactive: {e}")))?; check_local_exists(&p.path)?; let default_name = file_name_of(&p.path); if !default_name.to_lowercase().ends_with(".ifcfed") { - return Err(RpcError::invalid_params("push_ifcfed_interactive expects an .ifcfed file.")); + return Err(RpcError::invalid_params( + "push_ifcfed_interactive expects an .ifcfed file.", + )); } - let chosen = BrowseDialog::new(pair.auth.clone(), pair.aps.clone(), Mode::Destination).run()?; + let chosen = + BrowseDialog::new(pair.auth.clone(), pair.aps.clone(), Mode::Destination).run()?; let folder = chosen .entries .first() @@ -274,7 +303,11 @@ impl AutodeskConnector { let raw = prompt_for_filename("Save Project", "Save .ifcfed as:", &default_name) .ok_or_else(|| RpcError::internal("User cancelled save to cloud."))?; - let file_name = if raw.to_lowercase().ends_with(".ifcfed") { raw } else { format!("{raw}.ifcfed") }; + let file_name = if raw.to_lowercase().ends_with(".ifcfed") { + raw + } else { + format!("{raw}.ifcfed") + }; let project_id = chosen.project.id.clone(); let folder_id = folder.id.clone(); @@ -282,17 +315,28 @@ impl AutodeskConnector { let local_path = p.path.clone(); let upload_name = file_name.clone(); let uploaded = run_with_progress("Uploading project", move |report| { - aps.upload_file_to_folder(&project_id, &folder_id, &local_path, Some(&upload_name), Some(upload_callback(report))) + aps.upload_file_to_folder( + &project_id, + &folder_id, + &local_path, + Some(&upload_name), + Some(upload_callback(report)), + ) })?; - let cached_path = cache_ifcfed_locally(&chosen.project.id, &uploaded.item_id, &file_name, &p.path)?; - cache::write_manifest(&cached_path, &Manifest { - connector: CONNECTOR_ID.into(), - hub_id: chosen.hub.id, - project_id: chosen.project.id, - item_id: uploaded.item_id, - display_name: file_name, - }).map_err(|e| RpcError::internal(format!("Manifest write failed: {e}")))?; + let cached_path = + cache_ifcfed_locally(&chosen.project.id, &uploaded.item_id, &file_name, &p.path)?; + cache::write_manifest( + &cached_path, + &Manifest { + connector: CONNECTOR_ID.into(), + hub_id: chosen.hub.id, + project_id: chosen.project.id, + item_id: uploaded.item_id, + display_name: file_name, + }, + ) + .map_err(|e| RpcError::internal(format!("Manifest write failed: {e}")))?; Ok(json!({"path": cached_path.to_string_lossy()})) } @@ -300,23 +344,30 @@ impl AutodeskConnector { fn push_ifcfed(&self, params: Value) -> Result { let pair = self.require()?; - let p: PushIfcfedParams = serde_json::from_value(params).map_err(|e| { - RpcError::invalid_params(format!("push_ifcfed: {e}")) - })?; + let p: PushIfcfedParams = serde_json::from_value(params) + .map_err(|e| RpcError::invalid_params(format!("push_ifcfed: {e}")))?; check_local_exists(&p.path)?; if !file_name_of(&p.path).to_lowercase().ends_with(".ifcfed") { - return Err(RpcError::invalid_params("push_ifcfed expects an .ifcfed file.")); + return Err(RpcError::invalid_params( + "push_ifcfed expects an .ifcfed file.", + )); } if p.manifest.connector != CONNECTOR_ID { - return Err(RpcError::invalid_params(format!("Manifest connector is not '{CONNECTOR_ID}'."))); + return Err(RpcError::invalid_params(format!( + "Manifest connector is not '{CONNECTOR_ID}'." + ))); } - let item = pair.aps.get_item(&p.manifest.project_id, &p.manifest.item_id)?; + let item = pair + .aps + .get_item(&p.manifest.project_id, &p.manifest.item_id)?; ensure_visible(&item)?; - let folder_id = item - .parent_folder_id - .clone() - .ok_or_else(|| RpcError::internal(format!("Cannot resolve parent folder for item '{}'.", p.manifest.item_id)))?; + let folder_id = item.parent_folder_id.clone().ok_or_else(|| { + RpcError::internal(format!( + "Cannot resolve parent folder for item '{}'.", + p.manifest.item_id + )) + })?; let file_name = if p.manifest.display_name.is_empty() { item.display_name.clone() } else { @@ -328,17 +379,32 @@ impl AutodeskConnector { let project_id = p.manifest.project_id.clone(); let upload_name = file_name.clone(); let uploaded = run_with_progress("Uploading project", move |report| { - aps.upload_file_to_folder(&project_id, &folder_id, &local_path, Some(&upload_name), Some(upload_callback(report))) + aps.upload_file_to_folder( + &project_id, + &folder_id, + &local_path, + Some(&upload_name), + Some(upload_callback(report)), + ) })?; - let cached_path = cache_ifcfed_locally(&p.manifest.project_id, &uploaded.item_id, &file_name, &p.path)?; - cache::write_manifest(&cached_path, &Manifest { - connector: CONNECTOR_ID.into(), - hub_id: p.manifest.hub_id, - project_id: p.manifest.project_id, - item_id: uploaded.item_id, - display_name: file_name, - }).map_err(|e| RpcError::internal(format!("Manifest write failed: {e}")))?; + let cached_path = cache_ifcfed_locally( + &p.manifest.project_id, + &uploaded.item_id, + &file_name, + &p.path, + )?; + cache::write_manifest( + &cached_path, + &Manifest { + connector: CONNECTOR_ID.into(), + hub_id: p.manifest.hub_id, + project_id: p.manifest.project_id, + item_id: uploaded.item_id, + display_name: file_name, + }, + ) + .map_err(|e| RpcError::internal(format!("Manifest write failed: {e}")))?; Ok(json!({"path": cached_path.to_string_lossy()})) } @@ -346,12 +412,12 @@ impl AutodeskConnector { fn push_model_interactive(&self, params: Value) -> Result { let pair = self.require()?; - let p: PushInteractiveParams = serde_json::from_value(params).map_err(|e| { - RpcError::invalid_params(format!("push_model_interactive: {e}")) - })?; + let p: PushInteractiveParams = serde_json::from_value(params) + .map_err(|e| RpcError::invalid_params(format!("push_model_interactive: {e}")))?; check_local_exists(&p.path)?; - let chosen = BrowseDialog::new(pair.auth.clone(), pair.aps.clone(), Mode::Destination).run()?; + let chosen = + BrowseDialog::new(pair.auth.clone(), pair.aps.clone(), Mode::Destination).run()?; let folder = chosen .entries .first() @@ -368,10 +434,22 @@ impl AutodeskConnector { let local_path = p.path.clone(); let upload_name = file_name.clone(); let uploaded = run_with_progress("Uploading model", move |report| { - aps.upload_file_to_folder(&project_id, &folder_id, &local_path, Some(&upload_name), Some(upload_callback(report))) + aps.upload_file_to_folder( + &project_id, + &folder_id, + &local_path, + Some(&upload_name), + Some(upload_callback(report)), + ) })?; - let cached_path = cache_model_locally(&chosen.project.id, &uploaded.item_id, &uploaded.version_id, &file_name, &p.path)?; + let cached_path = cache_model_locally( + &chosen.project.id, + &uploaded.item_id, + &uploaded.version_id, + &file_name, + &p.path, + )?; Ok(json!({ "display_name": file_name, "path": cached_path.to_string_lossy(), @@ -389,31 +467,50 @@ impl AutodeskConnector { fn push_model(&self, params: Value) -> Result { let pair = self.require()?; - let p: PushModelParams = serde_json::from_value(params).map_err(|e| { - RpcError::invalid_params(format!("push_model: {e}")) - })?; + let p: PushModelParams = serde_json::from_value(params) + .map_err(|e| RpcError::invalid_params(format!("push_model: {e}")))?; check_local_exists(&p.path)?; if p.source.connector != CONNECTOR_ID { - return Err(RpcError::invalid_params(format!("Source connector is not '{CONNECTOR_ID}'."))); + return Err(RpcError::invalid_params(format!( + "Source connector is not '{CONNECTOR_ID}'." + ))); } let item = pair.aps.get_item(&p.source.project_id, &p.source.item_id)?; ensure_visible(&item)?; - let folder_id = item - .parent_folder_id - .clone() - .ok_or_else(|| RpcError::internal(format!("Cannot resolve parent folder for item '{}'.", p.source.item_id)))?; - let file_name = if item.display_name.is_empty() { file_name_of(&p.path) } else { item.display_name.clone() }; + let folder_id = item.parent_folder_id.clone().ok_or_else(|| { + RpcError::internal(format!( + "Cannot resolve parent folder for item '{}'.", + p.source.item_id + )) + })?; + let file_name = if item.display_name.is_empty() { + file_name_of(&p.path) + } else { + item.display_name.clone() + }; let aps = pair.aps.clone(); let local_path = p.path.clone(); let project_id = p.source.project_id.clone(); let upload_name = file_name.clone(); let uploaded = run_with_progress("Uploading model", move |report| { - aps.upload_file_to_folder(&project_id, &folder_id, &local_path, Some(&upload_name), Some(upload_callback(report))) + aps.upload_file_to_folder( + &project_id, + &folder_id, + &local_path, + Some(&upload_name), + Some(upload_callback(report)), + ) })?; - let _cached_path = cache_model_locally(&p.source.project_id, &uploaded.item_id, &uploaded.version_id, &file_name, &p.path)?; + let _cached_path = cache_model_locally( + &p.source.project_id, + &uploaded.item_id, + &uploaded.version_id, + &file_name, + &p.path, + )?; Ok(json!({ "source": Source { connector: CONNECTOR_ID.into(), @@ -438,10 +535,11 @@ fn download_ifcfed( ) -> Result { let item = aps.get_item(project_id, item_id)?; ensure_visible(&item)?; - let storage_id = item - .storage_id - .clone() - .ok_or_else(|| RpcError::internal(format!("Autodesk item '{item_id}' has no downloadable storage.")))?; + let storage_id = item.storage_id.clone().ok_or_else(|| { + RpcError::internal(format!( + "Autodesk item '{item_id}' has no downloadable storage." + )) + })?; let file_name = if !item.display_name.is_empty() { item.display_name.clone() } else if !display_name_hint.is_empty() { @@ -450,19 +548,25 @@ fn download_ifcfed( item_id.to_string() }; if !file_name.to_lowercase().ends_with(".ifcfed") { - return Err(RpcError::internal(format!("Item '{file_name}' is not an .ifcfed file."))); + return Err(RpcError::internal(format!( + "Item '{file_name}' is not an .ifcfed file." + ))); } let directory = cache::prepare_sole_child_dir(&cache::ifcfed_dir(project_id, item_id)) .map_err(|e| RpcError::internal(format!("Cache dir prep failed: {e}")))?; let ifcfed_path = directory.join(&file_name); aps.download_storage_to_file(&storage_id, &ifcfed_path, progress)?; - cache::write_manifest(&ifcfed_path, &Manifest { - connector: CONNECTOR_ID.into(), - hub_id: hub_id.into(), - project_id: project_id.into(), - item_id: item_id.into(), - display_name: file_name, - }).map_err(|e| RpcError::internal(format!("Manifest write failed: {e}")))?; + cache::write_manifest( + &ifcfed_path, + &Manifest { + connector: CONNECTOR_ID.into(), + hub_id: hub_id.into(), + project_id: project_id.into(), + item_id: item_id.into(), + display_name: file_name, + }, + ) + .map_err(|e| RpcError::internal(format!("Manifest write failed: {e}")))?; Ok(ifcfed_path) } @@ -472,7 +576,9 @@ fn resolve_scripted_model( progress: Option, ) -> Result, RpcError> { if entry.source.connector != CONNECTOR_ID { - return Err(RpcError::invalid_params(format!("Source connector is not '{CONNECTOR_ID}'."))); + return Err(RpcError::invalid_params(format!( + "Source connector is not '{CONNECTOR_ID}'." + ))); } let display_hint = entry .display_name @@ -481,14 +587,23 @@ fn resolve_scripted_model( let item = aps.get_item(&entry.source.project_id, &entry.source.item_id)?; if item.hidden { - eprintln!("Autodesk item '{}' is hidden/deleted; returning null.", entry.source.item_id); + eprintln!( + "Autodesk item '{}' is hidden/deleted; returning null.", + entry.source.item_id + ); return Ok(None); } - let storage_id = item - .storage_id - .clone() - .ok_or_else(|| RpcError::internal(format!("Autodesk item '{}' has no downloadable storage.", entry.source.item_id)))?; - let file_name = if item.display_name.is_empty() { display_hint } else { item.display_name.clone() }; + let storage_id = item.storage_id.clone().ok_or_else(|| { + RpcError::internal(format!( + "Autodesk item '{}' has no downloadable storage.", + entry.source.item_id + )) + })?; + let file_name = if item.display_name.is_empty() { + display_hint + } else { + item.display_name.clone() + }; let version_id = item.version_id.clone().unwrap_or_default(); let directory = cache::model_dir(&entry.source.project_id, &entry.source.item_id, &version_id); @@ -514,10 +629,11 @@ fn download_picked_model( ) -> Result, RpcError> { let item = aps.get_item(project_id, entry_id)?; ensure_visible(&item)?; - let storage_id = item - .storage_id - .clone() - .ok_or_else(|| RpcError::internal(format!("Autodesk item '{entry_id}' has no downloadable storage.")))?; + let storage_id = item.storage_id.clone().ok_or_else(|| { + RpcError::internal(format!( + "Autodesk item '{entry_id}' has no downloadable storage." + )) + })?; let file_name = if !item.display_name.is_empty() { item.display_name.clone() } else if !entry_display.is_empty() { @@ -548,20 +664,33 @@ fn download_picked_model( }))) } -fn cache_ifcfed_locally(project_id: &str, item_id: &str, file_name: &str, src: &Path) -> Result { +fn cache_ifcfed_locally( + project_id: &str, + item_id: &str, + file_name: &str, + src: &Path, +) -> Result { let directory = cache::prepare_sole_child_dir(&cache::ifcfed_dir(project_id, item_id)) .map_err(|e| RpcError::internal(format!("Cache dir prep failed: {e}")))?; let cached_path = directory.join(file_name); - std::fs::copy(src, &cached_path).map_err(|e| RpcError::internal(format!("Cache copy failed: {e}")))?; + std::fs::copy(src, &cached_path) + .map_err(|e| RpcError::internal(format!("Cache copy failed: {e}")))?; Ok(cached_path) } -fn cache_model_locally(project_id: &str, item_id: &str, version_id: &str, file_name: &str, src: &Path) -> Result { +fn cache_model_locally( + project_id: &str, + item_id: &str, + version_id: &str, + file_name: &str, + src: &Path, +) -> Result { let directory = cache::model_dir(project_id, item_id, version_id); cache::prepare_sole_child_dir(&directory) .map_err(|e| RpcError::internal(format!("Cache dir prep failed: {e}")))?; let cached_path = directory.join(file_name); - std::fs::copy(src, &cached_path).map_err(|e| RpcError::internal(format!("Cache copy failed: {e}")))?; + std::fs::copy(src, &cached_path) + .map_err(|e| RpcError::internal(format!("Cache copy failed: {e}")))?; Ok(cached_path) } @@ -569,7 +698,10 @@ fn build_metadata_from_item(item: &ItemTip) -> Value { let mut metadata = serde_json::Map::new(); if let Some(v) = &item.version_number { if !v.is_null() { - metadata.insert("revision".into(), json!(format!("v{}", json_value_to_display(v)))); + metadata.insert( + "revision".into(), + json!(format!("v{}", json_value_to_display(v))), + ); } } if let Some(s) = &item.last_modified_time_utc { @@ -589,7 +721,10 @@ fn build_metadata_from_upload(uploaded: &UploadResult) -> Value { let mut metadata = serde_json::Map::new(); if let Some(v) = &uploaded.version_number { if !v.is_null() { - metadata.insert("revision".into(), json!(format!("v{}", json_value_to_display(v)))); + metadata.insert( + "revision".into(), + json!(format!("v{}", json_value_to_display(v))), + ); } } if let Some(s) = &uploaded.last_modified_time_utc { @@ -616,14 +751,20 @@ fn json_value_to_display(v: &Value) -> String { fn ensure_visible(item: &ItemTip) -> Result<(), RpcError> { if item.hidden { - return Err(RpcError::internal(format!("Autodesk item '{}' has been deleted.", item.id))); + return Err(RpcError::internal(format!( + "Autodesk item '{}' has been deleted.", + item.id + ))); } Ok(()) } fn check_local_exists(path: &Path) -> Result<(), RpcError> { if !path.exists() { - return Err(RpcError::invalid_params(format!("Local file '{}' does not exist.", path.display()))); + return Err(RpcError::invalid_params(format!( + "Local file '{}' does not exist.", + path.display() + ))); } Ok(()) } diff --git a/src/bonsaiviewer-autodesk/src/progress.rs b/src/bonsaiviewer-autodesk/src/progress.rs index b708e45729..5984ee8b43 100644 --- a/src/bonsaiviewer-autodesk/src/progress.rs +++ b/src/bonsaiviewer-autodesk/src/progress.rs @@ -38,7 +38,9 @@ pub fn progress_detail(percent: Option, done: Option, total: Option 0 => parts.push(format!("{} / {}", format_bytes(d), format_bytes(t))), + (Some(d), Some(t)) if t > 0 => { + parts.push(format!("{} / {}", format_bytes(d), format_bytes(t))) + } (Some(d), _) => parts.push(format_bytes(d)), _ => {} } @@ -49,10 +51,18 @@ pub fn progress_detail(percent: Option, done: Option, total: Option ApsProgress { Arc::new(move |name, percent, done, total_bytes| { - let suffix = if total != 0 { format!(" ({index}/{total})") } else { String::new() }; + let suffix = if total != 0 { + format!(" ({index}/{total})") + } else { + String::new() + }; let detail = progress_detail(percent, done, total_bytes); let msg = format!("Downloading {name}{suffix}"); - let detail_ref = if detail.is_empty() { None } else { Some(detail.as_str()) }; + let detail_ref = if detail.is_empty() { + None + } else { + Some(detail.as_str()) + }; report("download", &msg, percent, detail_ref); }) } @@ -61,7 +71,11 @@ pub fn upload_callback(report: Report) -> ApsProgress { Arc::new(move |name, percent, done, total_bytes| { let detail = progress_detail(percent, done, total_bytes); let msg = format!("Uploading {name}"); - let detail_ref = if detail.is_empty() { None } else { Some(detail.as_str()) }; + let detail_ref = if detail.is_empty() { + None + } else { + Some(detail.as_str()) + }; report("upload", &msg, percent, detail_ref); }) } @@ -73,21 +87,31 @@ pub fn auth_to_report(report: Report) -> AuthProgress { }) } +/// A single coalesced report: `(phase, message, percent, detail)`. +type PendingReport = (String, String, Option, Option); + /// Latest-wins hand-off from worker thread → UI thread. Intermediate updates /// are coalesced: only the freshest matters for a progress bar. pub struct ProgressBridge { - pending: Mutex, Option)>>, + pending: Mutex>, } impl ProgressBridge { pub fn new() -> Arc { - Arc::new(Self { pending: Mutex::new(None) }) + Arc::new(Self { + pending: Mutex::new(None), + }) } pub fn report_fn(self: Arc) -> Report { Arc::new(move |phase, message, percent, detail| { let mut slot = self.pending.lock().unwrap(); - *slot = Some((phase.to_string(), message.to_string(), percent, detail.map(str::to_string))); + *slot = Some(( + phase.to_string(), + message.to_string(), + percent, + detail.map(str::to_string), + )); }) } @@ -110,7 +134,10 @@ mod tests { #[test] fn progress_detail_combines_parts() { - assert_eq!(progress_detail(Some(50), Some(512), Some(1024)), "50%, 512 B / 1.0 KB"); + assert_eq!( + progress_detail(Some(50), Some(512), Some(1024)), + "50%, 512 B / 1.0 KB" + ); assert_eq!(progress_detail(None, Some(512), None), "512 B"); assert_eq!(progress_detail(None, None, None), ""); } diff --git a/src/bonsaiviewer-autodesk/src/rpc.rs b/src/bonsaiviewer-autodesk/src/rpc.rs index 1561fc47e0..80fa172a58 100644 --- a/src/bonsaiviewer-autodesk/src/rpc.rs +++ b/src/bonsaiviewer-autodesk/src/rpc.rs @@ -18,7 +18,11 @@ pub struct RpcError { impl RpcError { pub fn new(code: i32, message: impl Into) -> Self { - Self { code, message: message.into(), data: None } + Self { + code, + message: message.into(), + data: None, + } } pub fn internal(message: impl Into) -> Self { @@ -48,7 +52,11 @@ pub struct JsonRpcHost { impl JsonRpcHost { pub fn new(handlers: HashMap, stdin: R, stdout: W) -> Self { - Self { handlers, stdin: BufReader::new(stdin), stdout } + Self { + handlers, + stdin: BufReader::new(stdin), + stdout, + } } pub fn run(&mut self) -> i32 { @@ -72,7 +80,12 @@ impl JsonRpcHost { let message: Value = match serde_json::from_str(line) { Ok(v) => v, Err(e) => { - self.respond_error(Value::Null, JSONRPC_PARSE_ERROR, &format!("Parse error: {e}"), None); + self.respond_error( + Value::Null, + JSONRPC_PARSE_ERROR, + &format!("Parse error: {e}"), + None, + ); return; } }; @@ -80,7 +93,12 @@ impl JsonRpcHost { let obj = match message.as_object() { Some(o) => o, None => { - self.respond_error(Value::Null, JSONRPC_INVALID_REQUEST, "Request must be a JSON object", None); + self.respond_error( + Value::Null, + JSONRPC_INVALID_REQUEST, + "Request must be a JSON object", + None, + ); return; } }; @@ -88,28 +106,48 @@ impl JsonRpcHost { let message_id = obj.get("id").cloned().unwrap_or(Value::Null); if obj.get("jsonrpc").and_then(|v| v.as_str()) != Some("2.0") { - self.respond_error(message_id, JSONRPC_INVALID_REQUEST, "Missing or wrong 'jsonrpc' version", None); + self.respond_error( + message_id, + JSONRPC_INVALID_REQUEST, + "Missing or wrong 'jsonrpc' version", + None, + ); return; } let method = match obj.get("method").and_then(|v| v.as_str()) { Some(m) => m.to_string(), None => { - self.respond_error(message_id, JSONRPC_INVALID_REQUEST, "Missing 'method' string", None); + self.respond_error( + message_id, + JSONRPC_INVALID_REQUEST, + "Missing 'method' string", + None, + ); return; } }; let params = obj.get("params").cloned().unwrap_or(Value::Null); if !params.is_null() && !params.is_object() && !params.is_array() { - self.respond_error(message_id, JSONRPC_INVALID_PARAMS, "'params' must be a JSON object or array", None); + self.respond_error( + message_id, + JSONRPC_INVALID_PARAMS, + "'params' must be a JSON object or array", + None, + ); return; } let outcome = match self.handlers.get(&method) { Some(handler) => handler(params), None => { - self.respond_error(message_id, JSONRPC_METHOD_NOT_FOUND, &format!("Unknown method '{method}'"), None); + self.respond_error( + message_id, + JSONRPC_METHOD_NOT_FOUND, + &format!("Unknown method '{method}'"), + None, + ); return; } }; @@ -168,15 +206,21 @@ mod tests { fn echo_handlers() -> HashMap { let mut m: HashMap = HashMap::new(); - m.insert("echo".into(), Box::new(|p| Ok(p))); + m.insert("echo".into(), Box::new(Ok)); m.insert("boom".into(), Box::new(|_| Err(RpcError::internal("bang")))); - m.insert("bad_params".into(), Box::new(|_| Err(RpcError::invalid_params("nope")))); + m.insert( + "bad_params".into(), + Box::new(|_| Err(RpcError::invalid_params("nope"))), + ); m } #[test] fn dispatches_result() { - let out = run_once(echo_handlers(), "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"echo\",\"params\":{\"x\":1}}\n"); + let out = run_once( + echo_handlers(), + "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"echo\",\"params\":{\"x\":1}}\n", + ); assert_eq!(out.len(), 1); assert_eq!(out[0]["result"], json!({"x": 1})); assert_eq!(out[0]["id"], json!(1)); @@ -184,13 +228,19 @@ mod tests { #[test] fn notification_no_response() { - let out = run_once(echo_handlers(), "{\"jsonrpc\":\"2.0\",\"method\":\"echo\",\"params\":{}}\n"); + let out = run_once( + echo_handlers(), + "{\"jsonrpc\":\"2.0\",\"method\":\"echo\",\"params\":{}}\n", + ); assert!(out.is_empty()); } #[test] fn unknown_method() { - let out = run_once(echo_handlers(), "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"nope\"}\n"); + let out = run_once( + echo_handlers(), + "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"nope\"}\n", + ); assert_eq!(out[0]["error"]["code"], json!(JSONRPC_METHOD_NOT_FOUND)); } @@ -209,13 +259,19 @@ mod tests { #[test] fn invalid_params_shape() { - let out = run_once(echo_handlers(), "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"echo\",\"params\":42}\n"); + let out = run_once( + echo_handlers(), + "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"echo\",\"params\":42}\n", + ); assert_eq!(out[0]["error"]["code"], json!(JSONRPC_INVALID_PARAMS)); } #[test] fn handler_error_passes_code() { - let out = run_once(echo_handlers(), "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"bad_params\"}\n"); + let out = run_once( + echo_handlers(), + "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"bad_params\"}\n", + ); assert_eq!(out[0]["error"]["code"], json!(JSONRPC_INVALID_PARAMS)); assert_eq!(out[0]["error"]["message"], json!("nope")); } diff --git a/src/bonsaiviewer-autodesk/src/ui/browse.rs b/src/bonsaiviewer-autodesk/src/ui/browse.rs index 7c0d52beef..a82112d8fe 100644 --- a/src/bonsaiviewer-autodesk/src/ui/browse.rs +++ b/src/bonsaiviewer-autodesk/src/ui/browse.rs @@ -64,10 +64,10 @@ const PLACEHOLDER_LABEL: &str = "Loading…"; /// cannot touch FLTK widgets. #[derive(Clone, Debug)] enum WorkerMsg { - HubsLoaded(Result, RpcError>), - ProjectsLoaded(Result, RpcError>), - TopFoldersLoaded(Result, RpcError>), - FolderContentsLoaded(String, Result, RpcError>), + Hubs(Result, RpcError>), + Projects(Result, RpcError>), + TopFolders(Result, RpcError>), + FolderContents(String, Result, RpcError>), } struct State { @@ -198,7 +198,6 @@ impl BrowseDialog { sign_in.set_callback({ let aps = aps.clone(); let auth = auth.clone(); - let worker_tx = worker_tx; let mut status = status_mut.clone(); move |_| { status.set_label("Signing in to Autodesk…"); @@ -208,7 +207,7 @@ impl BrowseDialog { let outcome = auth .login_interactive(noop_auth()) .and_then(|_| aps.list_hubs()); - worker_tx.send(WorkerMsg::HubsLoaded(outcome)); + worker_tx.send(WorkerMsg::Hubs(outcome)); }); } }); @@ -216,7 +215,6 @@ impl BrowseDialog { hub_combo.set_callback({ let aps = aps.clone(); let state = state.clone(); - let worker_tx = worker_tx; let mut projects_browser = projects_browser.clone(); let mut tree = tree.clone(); let mut status = status_mut.clone(); @@ -247,7 +245,7 @@ impl BrowseDialog { status.set_label(&format!("Loading projects in {hub_name}…")); let aps = aps.clone(); thread::spawn(move || { - worker_tx.send(WorkerMsg::ProjectsLoaded(aps.list_projects(&hub_id))); + worker_tx.send(WorkerMsg::Projects(aps.list_projects(&hub_id))); }); } }); @@ -255,7 +253,6 @@ impl BrowseDialog { projects_browser.clone().set_callback({ let aps = aps.clone(); let state = state.clone(); - let worker_tx = worker_tx; let mut tree = tree.clone(); let mut status = status_mut.clone(); let action_btn_clone = action_btn.clone(); @@ -286,8 +283,9 @@ impl BrowseDialog { status.set_label(&format!("Loading top folders in {project_name}…")); let aps = aps.clone(); thread::spawn(move || { - worker_tx - .send(WorkerMsg::TopFoldersLoaded(aps.list_top_folders(&hub_id, &project_id))); + worker_tx.send(WorkerMsg::TopFolders( + aps.list_top_folders(&hub_id, &project_id), + )); }); } }); @@ -295,7 +293,6 @@ impl BrowseDialog { tree.clone().set_callback({ let aps = aps.clone(); let state = state.clone(); - let worker_tx = worker_tx; let mut status = status_mut.clone(); let action_btn_clone = action_btn.clone(); move |t| match t.callback_reason() { @@ -306,10 +303,16 @@ impl BrowseDialog { refresh_action_button(&mut action_btn_clone.clone(), &s, mode); } TreeReason::Opened => { - let Some(item) = t.callback_item() else { return }; - let Ok(path) = t.item_pathname(&item) else { return }; + let Some(item) = t.callback_item() else { + return; + }; + let Ok(path) = t.item_pathname(&item) else { + return; + }; let mut s = state.borrow_mut(); - let Some(entry) = s.tree_entries.get(&path).cloned() else { return }; + let Some(entry) = s.tree_entries.get(&path).cloned() else { + return; + }; if entry.entry_type != EntryType::Folders { return; } @@ -334,8 +337,9 @@ impl BrowseDialog { let folder_id = entry.id.clone(); let aps = aps.clone(); thread::spawn(move || { - let result = list_folder_contents_for_mode(&aps, &project_id, &folder_id, mode); - worker_tx.send(WorkerMsg::FolderContentsLoaded(path, result)); + let result = + list_folder_contents_for_mode(&aps, &project_id, &folder_id, mode); + worker_tx.send(WorkerMsg::FolderContents(path, result)); }); } _ => {} @@ -373,7 +377,7 @@ impl BrowseDialog { if auth.get_token().ok().flatten().is_some() { let aps = aps.clone(); thread::spawn(move || { - worker_tx.send(WorkerMsg::HubsLoaded(aps.list_hubs())); + worker_tx.send(WorkerMsg::Hubs(aps.list_hubs())); }); } @@ -416,15 +420,15 @@ fn apply_worker_msg( mode: Mode, ) { match msg { - WorkerMsg::HubsLoaded(Err(e)) => show_error("Sign In Failed", &e.message), - WorkerMsg::HubsLoaded(Ok(hubs)) => { + WorkerMsg::Hubs(Err(e)) => show_error("Sign In Failed", &e.message), + WorkerMsg::Hubs(Ok(hubs)) => { let mut s = state.borrow_mut(); s.hubs = hubs; refill_hub_combo(hub_combo, &s.hubs); status.set_label("Signed in. Select a hub."); } - WorkerMsg::ProjectsLoaded(Err(e)) => show_error("Load Projects Failed", &e.message), - WorkerMsg::ProjectsLoaded(Ok(projects)) => { + WorkerMsg::Projects(Err(e)) => show_error("Load Projects Failed", &e.message), + WorkerMsg::Projects(Ok(projects)) => { let mut s = state.borrow_mut(); s.projects = projects; projects_browser.clear(); @@ -437,8 +441,8 @@ fn apply_worker_msg( } } } - WorkerMsg::TopFoldersLoaded(Err(e)) => show_error("Load Project Failed", &e.message), - WorkerMsg::TopFoldersLoaded(Ok(folders)) => { + WorkerMsg::TopFolders(Err(e)) => show_error("Load Project Failed", &e.message), + WorkerMsg::TopFolders(Ok(folders)) => { let mut s = state.borrow_mut(); for entry in folders { insert_tree_entry(tree, &mut s.tree_entries, "", &entry); @@ -457,11 +461,11 @@ fn apply_worker_msg( } } } - WorkerMsg::FolderContentsLoaded(parent, Err(e)) => { + WorkerMsg::FolderContents(parent, Err(e)) => { state.borrow_mut().loaded_folders.remove(&parent); show_error("Load Folder Failed", &e.message); } - WorkerMsg::FolderContentsLoaded(parent, Ok(children)) => { + WorkerMsg::FolderContents(parent, Ok(children)) => { let mut s = state.borrow_mut(); for entry in &children { insert_tree_entry(tree, &mut s.tree_entries, &parent, entry); @@ -474,6 +478,9 @@ fn apply_worker_msg( // ---- worker helpers ------------------------------------------------------- +/// Per-mode predicate deciding which listed entries survive. +type EntryFilter = Box bool>; + fn list_folder_contents_for_mode( aps: &Arc, project_id: &str, @@ -485,7 +492,7 @@ fn list_folder_contents_for_mode( } else { &["folders", "items"] }; - let filter_box: Option bool>> = match mode { + let filter_box: Option = match mode { Mode::Ifcfed => Some(Box::new(|e: &Entry| { e.display_name.to_lowercase().ends_with(".ifcfed") })), @@ -517,7 +524,9 @@ fn insert_tree_entry( parent_canonical: &str, entry: &Entry, ) -> String { - let parent_add = parent_canonical.strip_prefix('/').unwrap_or(parent_canonical); + let parent_add = parent_canonical + .strip_prefix('/') + .unwrap_or(parent_canonical); let label = if entry.display_name.is_empty() { entry.id.clone() } else { @@ -565,7 +574,9 @@ fn add_unique(tree: &mut Tree, parent_add: &str, label: &str) -> String { } fn collect_selected_entries(tree: &Tree, tree_entries: &HashMap) -> Vec { - let Some(items) = tree.get_selected_items() else { return Vec::new() }; + let Some(items) = tree.get_selected_items() else { + return Vec::new(); + }; items .into_iter() .filter_map(|item| tree.item_pathname(&item).ok()) @@ -586,7 +597,10 @@ fn update_status_for_selection(status: &mut Frame, entries: &[Entry], mode: Mode status.set_label(&format!("Selected {kind}: {}", entry.display_name)); } n => { - let valid = entries.iter().filter(|e| is_valid_selection(mode, e)).count(); + let valid = entries + .iter() + .filter(|e| is_valid_selection(mode, e)) + .count(); status.set_label(&format!("Selected {valid} of {n} items.")); } } @@ -636,11 +650,29 @@ mod tests { #[test] fn is_valid_selection_modes() { - assert!(is_valid_selection(Mode::Ifcfed, &entry("a.ifcfed", EntryType::Items))); - assert!(!is_valid_selection(Mode::Ifcfed, &entry("a.ifc", EntryType::Items))); - assert!(is_valid_selection(Mode::Model, &entry("a.ifc", EntryType::Items))); - assert!(is_valid_selection(Mode::Model, &entry("a.rdb", EntryType::Items))); - assert!(is_valid_selection(Mode::Destination, &entry("folder", EntryType::Folders))); - assert!(!is_valid_selection(Mode::Destination, &entry("file.ifc", EntryType::Items))); + assert!(is_valid_selection( + Mode::Ifcfed, + &entry("a.ifcfed", EntryType::Items) + )); + assert!(!is_valid_selection( + Mode::Ifcfed, + &entry("a.ifc", EntryType::Items) + )); + assert!(is_valid_selection( + Mode::Model, + &entry("a.ifc", EntryType::Items) + )); + assert!(is_valid_selection( + Mode::Model, + &entry("a.rdb", EntryType::Items) + )); + assert!(is_valid_selection( + Mode::Destination, + &entry("folder", EntryType::Folders) + )); + assert!(!is_valid_selection( + Mode::Destination, + &entry("file.ifc", EntryType::Items) + )); } } diff --git a/src/bonsaiviewer-autodesk/src/ui/settings.rs b/src/bonsaiviewer-autodesk/src/ui/settings.rs index a815579fbb..1bf7e91bb0 100644 --- a/src/bonsaiviewer-autodesk/src/ui/settings.rs +++ b/src/bonsaiviewer-autodesk/src/ui/settings.rs @@ -1,15 +1,9 @@ //! Settings dialog: APS client id + OAuth callback port + sign-out. -use std::sync::Arc; +use std::rc::Rc; use fltk::{ - app, - button::Button, - enums::Align, - frame::Frame, - group::Flex, - input::Input, - prelude::*, + app, button::Button, enums::Align, frame::Frame, group::Flex, input::Input, prelude::*, window::Window, }; @@ -20,11 +14,11 @@ use crate::ui::dialogs::{center_on_screen, confirm, drain_after_close, show_erro use crate::ui::ensure_app; pub struct SettingsDialog { - pub on_reload: Arc Result<(), RpcError>>, + pub on_reload: Rc Result<(), RpcError>>, } impl SettingsDialog { - pub fn new(on_reload: Arc Result<(), RpcError>>) -> Self { + pub fn new(on_reload: Rc Result<(), RpcError>>) -> Self { Self { on_reload } } diff --git a/src/bonsaiviewer-autodesk/tests/aps.rs b/src/bonsaiviewer-autodesk/tests/aps.rs index 71a18f61dd..eae56dd1e1 100644 --- a/src/bonsaiviewer-autodesk/tests/aps.rs +++ b/src/bonsaiviewer-autodesk/tests/aps.rs @@ -57,7 +57,10 @@ fn spawn_stub(routes: Vec<(&'static str, &'static str, u16, String)>) -> StubSer if h == "\r\n" || h == "\n" { break; } - if let Some(v) = h.strip_prefix("Content-Length: ").or_else(|| h.strip_prefix("content-length: ")) { + if let Some(v) = h + .strip_prefix("Content-Length: ") + .or_else(|| h.strip_prefix("content-length: ")) + { content_length = v.trim().parse().unwrap_or(0); } } @@ -72,7 +75,9 @@ fn spawn_stub(routes: Vec<(&'static str, &'static str, u16, String)>) -> StubSer let method = parts.first().copied().unwrap_or(""); let path = parts.get(1).copied().unwrap_or("/"); - let idx = routes.iter().position(|(m, p, _, _)| *m == method && path.starts_with(p)); + let idx = routes + .iter() + .position(|(m, p, _, _)| *m == method && path.starts_with(p)); let (status, body) = match idx { Some(i) => { let (_, _, status, body) = routes.remove(i); @@ -156,9 +161,15 @@ fn list_folder_contents_filters_ifcfed() { {"id": "i2", "type": "items", "attributes": {"displayName": "model.ifcfed"}} ] }"#; - let server = spawn_stub(vec![("GET", "/data/v1/projects/P/folders/F/contents", 200, body.into())]); + let server = spawn_stub(vec![( + "GET", + "/data/v1/projects/P/folders/F/contents", + 200, + body.into(), + )]); let client = make_client(server.base_url.clone()); - let filter = |e: &bonsaiviewer_autodesk::aps::Entry| e.display_name.to_lowercase().ends_with(".ifcfed"); + let filter = + |e: &bonsaiviewer_autodesk::aps::Entry| e.display_name.to_lowercase().ends_with(".ifcfed"); let entries = client .list_folder_contents("P", "F", &["folders", "items"], Some(&filter)) .unwrap(); @@ -193,12 +204,20 @@ fn get_item_returns_tip_metadata() { } }] }"#; - let server = spawn_stub(vec![("GET", "/data/v1/projects/P/items/I", 200, body.into())]); + let server = spawn_stub(vec![( + "GET", + "/data/v1/projects/P/items/I", + 200, + body.into(), + )]); let client = make_client(server.base_url.clone()); let item = client.get_item("P", "I").unwrap(); assert!(!item.hidden); assert_eq!(item.version_id.as_deref(), Some("V")); - assert_eq!(item.storage_id.as_deref(), Some("urn:adsk.objects:os.object:bk/obj")); + assert_eq!( + item.storage_id.as_deref(), + Some("urn:adsk.objects:os.object:bk/obj") + ); assert_eq!(item.last_modified_user_name.as_deref(), Some("alice")); assert_eq!(item.parent_folder_id.as_deref(), Some("F")); } @@ -216,7 +235,12 @@ fn get_item_handles_missing_tip_as_hidden() { } } }"#; - let server = spawn_stub(vec![("GET", "/data/v1/projects/P/items/I", 200, body.into())]); + let server = spawn_stub(vec![( + "GET", + "/data/v1/projects/P/items/I", + 200, + body.into(), + )]); let client = make_client(server.base_url.clone()); let item = client.get_item("P", "I").unwrap(); assert!(item.hidden); @@ -228,10 +252,7 @@ fn get_item_handles_missing_tip_as_hidden() { fn download_signed_then_get_writes_file() { let tmp = tempfile::tempdir().unwrap(); let dst = tmp.path().join("out.bin"); - let signed = format!( - r#"{{"url": "{base}/payload"}}"#, - base = "PLACEHOLDER" - ); + let signed = format!(r#"{{"url": "{base}/payload"}}"#, base = "PLACEHOLDER"); // We need two-stage: first signed url returns a pointer into the same // stub, then the GET against that path returns the bytes. let listener = TcpListener::bind("127.0.0.1:0").unwrap(); @@ -240,8 +261,18 @@ fn download_signed_then_get_writes_file() { let signed_with_url = signed.replace("PLACEHOLDER", &base); let payload_bytes = b"hello-bytes-12345"; let mut routes: Vec<(&'static str, &'static str, u16, String)> = vec![ - ("GET", "/oss/v2/buckets/bk/objects/obj/signeds3download", 200, signed_with_url), - ("GET", "/payload", 200, String::from_utf8(payload_bytes.to_vec()).unwrap()), + ( + "GET", + "/oss/v2/buckets/bk/objects/obj/signeds3download", + 200, + signed_with_url, + ), + ( + "GET", + "/payload", + 200, + String::from_utf8(payload_bytes.to_vec()).unwrap(), + ), ]; // Hand-roll a stub on the bound listener so we can re-use the same port. let handle = thread::spawn(move || { @@ -262,7 +293,10 @@ fn download_signed_then_get_writes_file() { let parts: Vec<&str> = request_line.split_whitespace().collect(); let method = parts[0]; let path = parts[1]; - let idx = routes.iter().position(|(m, p, _, _)| *m == method && path.starts_with(p)).unwrap(); + let idx = routes + .iter() + .position(|(m, p, _, _)| *m == method && path.starts_with(p)) + .unwrap(); let (_, _, status, body) = routes.remove(idx); let header = format!( "HTTP/1.1 {status} OK\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", @@ -298,10 +332,12 @@ fn upload_small_file_creates_new_item() { let base = format!("http://127.0.0.1:{port}"); let presigned_url = format!("{base}/presigned/0"); - let put_bytes_received: Arc>> = Arc::new(std::sync::Mutex::new(Vec::new())); + let put_bytes_received: Arc>> = + Arc::new(std::sync::Mutex::new(Vec::new())); let put_bytes_clone = put_bytes_received.clone(); - let create_storage_body = r#"{"data":{"id":"urn:adsk.objects:os.object:bk/obj","type":"objects"}}"#.to_string(); + let create_storage_body = + r#"{"data":{"id":"urn:adsk.objects:os.object:bk/obj","type":"objects"}}"#.to_string(); let signed_upload_body = format!(r#"{{"uploadKey":"UK1","urls":["{presigned_url}"]}}"#); let complete_body = r#"{}"#.to_string(); let folder_contents_body = r#"{"data":[]}"#.to_string(); @@ -316,10 +352,22 @@ fn upload_small_file_creates_new_item() { let handle = thread::spawn(move || { let expected = vec![ ("POST", "/data/v1/projects/P/storage", create_storage_body), - ("GET", "/oss/v2/buckets/bk/objects/obj/signeds3upload", signed_upload_body), + ( + "GET", + "/oss/v2/buckets/bk/objects/obj/signeds3upload", + signed_upload_body, + ), ("PUT", "/presigned/0", String::new()), - ("POST", "/oss/v2/buckets/bk/objects/obj/signeds3upload", complete_body), - ("GET", "/data/v1/projects/P/folders/F/contents", folder_contents_body), + ( + "POST", + "/oss/v2/buckets/bk/objects/obj/signeds3upload", + complete_body, + ), + ( + "GET", + "/data/v1/projects/P/folders/F/contents", + folder_contents_body, + ), ("POST", "/data/v1/projects/P/items", create_item_body), ]; for (method, prefix, body) in expected { @@ -350,7 +398,11 @@ fn upload_small_file_creates_new_item() { } let parts: Vec<&str> = request_line.split_whitespace().collect(); assert_eq!(parts[0], method, "request_line={request_line:?}"); - assert!(parts[1].starts_with(prefix), "got {} expected prefix {prefix}", parts[1]); + assert!( + parts[1].starts_with(prefix), + "got {} expected prefix {prefix}", + parts[1] + ); let header = format!( "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", body.len() diff --git a/src/bonsaiviewer-autodesk/tests/rpc_smoke.rs b/src/bonsaiviewer-autodesk/tests/rpc_smoke.rs index bc1a10838f..6a1b8c7af7 100644 --- a/src/bonsaiviewer-autodesk/tests/rpc_smoke.rs +++ b/src/bonsaiviewer-autodesk/tests/rpc_smoke.rs @@ -10,14 +10,26 @@ use serde_json::Value; fn binary_path() -> std::path::PathBuf { let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); path.push("target"); - path.push(if cfg!(debug_assertions) { "debug" } else { "release" }); - path.push(if cfg!(windows) { "bonsaiviewer-autodesk.exe" } else { "bonsaiviewer-autodesk" }); + path.push(if cfg!(debug_assertions) { + "debug" + } else { + "release" + }); + path.push(if cfg!(windows) { + "bonsaiviewer-autodesk.exe" + } else { + "bonsaiviewer-autodesk" + }); path } fn exchange(requests: &[&str]) -> Vec { let path = binary_path(); - assert!(path.exists(), "expected binary at {}; build with `cargo build --bin bonsaiviewer-autodesk` first", path.display()); + assert!( + path.exists(), + "expected binary at {}; build with `cargo build --bin bonsaiviewer-autodesk` first", + path.display() + ); let mut child = Command::new(&path) .stdin(Stdio::piped()) .stdout(Stdio::piped())