From 17facef7e90e024024a967ef1303ce976f0b35b9 Mon Sep 17 00:00:00 2001 From: Marco Neumann Date: Sun, 22 Sep 2024 17:44:17 +0200 Subject: [PATCH] feat: add easy way to dump JSON data for debugging --- Cargo.lock | 10 ++++++++++ Cargo.toml | 1 + src/client.rs | 49 +++++++++++++++++++++++++++++++++++++++++++++---- src/main.rs | 10 +++++++++- tests/cli.rs | 18 ++++++++++++++++++ 5 files changed, 83 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c709c8a..f95ef86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1783,6 +1783,7 @@ dependencies = [ "tracing", "tracing-log", "tracing-subscriber", + "uuid", ] [[package]] @@ -2072,6 +2073,15 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +dependencies = [ + "getrandom", +] + [[package]] name = "valuable" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index d16abe8..5135355 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ tokio-retry = "0.3.0" tracing = "0.1.38" tracing-log = "0.2.0" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } +uuid = { version = "1.10.0", features = ["v4"] } [dev-dependencies] assert_cmd = "2.0.16" diff --git a/src/client.rs b/src/client.rs index 6f16913..423c2d0 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,4 +1,4 @@ -use std::{future::Future, sync::Arc}; +use std::{future::Future, path::PathBuf, sync::Arc}; use anyhow::{Context, Result}; use futures::Stream; @@ -9,6 +9,7 @@ use tokio::{ task::JoinSet, }; use tracing::{debug, warn}; +use uuid::Uuid; use crate::{ constants::APP_USER_AGENT, @@ -22,10 +23,11 @@ pub(crate) const DEFAULT_HOST: &str = "https://app.tuta.com"; #[derive(Debug, Clone)] pub(crate) struct Client { inner: reqwest::Client, + debug_dump_json_to: Option, } impl Client { - pub(crate) fn try_new() -> Result { + pub(crate) async fn try_new(debug_dump_json_to: Option) -> Result { let inner = reqwest::Client::builder() .hickory_dns(true) .http2_adaptive_window(true) @@ -36,7 +38,16 @@ impl Client { .build() .context("set up HTTPs client")?; - Ok(Self { inner }) + if let Some(path) = &debug_dump_json_to { + tokio::fs::create_dir_all(path) + .await + .context("creating directories to dump JSON data")?; + } + + Ok(Self { + inner, + debug_dump_json_to, + }) } pub(crate) fn stream( @@ -121,10 +132,40 @@ impl Client { { let s = retry(|| async { self.do_request(r.clone()).await?.text().await }).await?; + let json_path = match &self.debug_dump_json_to { + Some(path) => { + let uuid = Uuid::new_v4(); + let path = path.join(format!("{uuid}.json")); + debug!(%uuid, path=%path.display(), "dumping debug JSON"); + tokio::fs::write(&path, &s) + .await + .context("dumping debug JSON")?; + Some(path) + } + None => None, + }; + let jd = &mut serde_json::Deserializer::from_str(&s); let res: Result = serde_path_to_error::deserialize(jd); - res.with_context(|| format!("deserialize JSON for `{}`", std::any::type_name::())) + res.with_context(|| { + let type_name = std::any::type_name::(); + match json_path { + Some(json_path) => { + format!( + "deserialize JSON for `{}`, data dumped to `{}`", + type_name, + json_path.display(), + ) + } + None => { + format!( + "deserialize JSON for `{}`, consider passing `--debug-dump-json-to=some/path` to dump the data", + type_name, + ) + } + } + }) } pub(crate) async fn do_bytes(&self, r: Request<'_, Req>) -> Result> diff --git a/src/main.rs b/src/main.rs index 3714308..f0f7ffb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -45,6 +45,12 @@ struct Args { #[clap(flatten)] logging_cfg: LoggingCLIConfig, + /// Dump JSON responses of server to given folder. + /// + /// This is useful for development and debugging. + #[clap(long)] + debug_dump_json_to: Option, + /// Login config. #[clap(flatten)] login_cfg: LoginCLIConfig, @@ -85,7 +91,9 @@ async fn main() -> Result<()> { let args = Args::parse(); setup_logging(args.logging_cfg).context("logging setup")?; - let client = Client::try_new().context("set up client")?; + let client = Client::try_new(args.debug_dump_json_to) + .await + .context("set up client")?; let session = Session::login(args.login_cfg, &client) .await diff --git a/tests/cli.rs b/tests/cli.rs index 6634dcb..e861825 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -42,6 +42,24 @@ fn read_files(path: &Path) -> HashMap { mod integration { use super::*; + #[test] + fn test_debug_dump_json() { + let tmp_dir = TempDir::new().unwrap(); + + // use path that does NOT exist + let dump_dir = tmp_dir.path().join("json"); + + let mut cmd = cmd(); + cmd.arg("-vv") + .arg("--debug-dump-json-to") + .arg(&dump_dir) + .arg("list-folders") + .assert() + .success(); + + assert!(std::fs::read_dir(dump_dir).unwrap().count() > 0); + } + #[test] fn test_list_folders() { let mut cmd = cmd();