From 8c318a7ecf8da8fe39df0abdb3935ce41b421ea5 Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Tue, 4 Nov 2025 02:25:04 +0000 Subject: [PATCH 01/49] refactor: replace settings management with a new structure and file-based storage --- src/settings.rs | 136 -------------------------------------------- src/settings/mod.rs | 83 +++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 136 deletions(-) delete mode 100644 src/settings.rs create mode 100644 src/settings/mod.rs diff --git a/src/settings.rs b/src/settings.rs deleted file mode 100644 index 580fd7c..0000000 --- a/src/settings.rs +++ /dev/null @@ -1,136 +0,0 @@ -use anyhow::{Context, Result}; -use directories::UserDirs; -use serde::{Deserialize, Serialize}; -use std::fs; -use std::io::prelude::*; - -#[derive(Serialize, Deserialize, Debug)] -pub struct Settings { - #[serde(rename = "clientId")] - pub customer_id: Option, - #[serde(rename = "password")] - pub password: Option, -} - -#[cfg(not(tarpaulin_include))] -impl Settings { - pub fn load(path: &str) -> Result { - let file_content = match fs::read_to_string(path) { - Ok(data) => data, - Err(_) => { - return Err(anyhow::anyhow!("Failed to read settings file")); - } - }; - - let settings: Settings = serde_json::from_str(&file_content).map_err(|e| { - anyhow::anyhow!( - "Failed to deserialize settings: {}\nPlease make sure the settings file is valid.", - e - ) - })?; - - Ok(settings) - } -} - -#[cfg(not(tarpaulin_include))] -pub fn get_settings() -> Result { - let user_dirs = UserDirs::new().context("Failed to get user directories")?; - let mut path = user_dirs.home_dir().to_path_buf(); - path = path.join(".bourso/settings.json"); - let file_content = match fs::read_to_string(&path) { - Ok(data) => data, - Err(_) => { - // Create the settings file if it doesn't exist - save_settings(&Settings { - customer_id: None, - password: None, - })?; - return Ok(Settings { - customer_id: None, - password: None, - }); - } - }; - - let settings: Settings = serde_json::from_str(&file_content).map_err(|e| { - anyhow::anyhow!( - "Failed to deserialize settings: {}\nPlease make sure the settings file is valid.", - e - ) - })?; - Ok(settings) -} - -/// Save the settings to the settings file, if it doesn't exist, create it -#[cfg(not(tarpaulin_include))] -pub fn save_settings(settings: &Settings) -> Result<()> { - let user_dirs = UserDirs::new().context("Failed to get user directories")?; - let mut path = user_dirs.home_dir().to_path_buf(); - // Create the .bourso directory if it doesn't exist - path = path.join(".bourso"); - fs::create_dir_all(&path)?; - path = path.join("settings.json"); - let mut file = fs::File::create(&path).context("Failed to create settings file")?; - let json = serde_json::to_string_pretty(settings).context("Failed to serialize settings")?; - file.write_all(json.as_bytes()) - .context("Failed to write settings file")?; - Ok(()) -} - -pub fn init_logger() -> Result<()> { - use std::io::IsTerminal; - use std::{fs, io}; - use tracing_subscriber::filter::LevelFilter; - use tracing_subscriber::{fmt, prelude::*, EnvFilter}; - - let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); - - // Create ~/.bourso/bourso.log if it doesn't exist - let user_dirs = UserDirs::new().context("Failed to get user directories")?; - let mut path = user_dirs.home_dir().to_path_buf(); - path.push(".bourso"); - fs::create_dir_all(&path)?; - path.push("bourso.log"); - - // Pretty console (stderr), filtered by RUST_LOG - let console_layer = fmt::layer() - .with_writer(io::stderr) - .with_ansi(IsTerminal::is_terminal(&io::stderr())) - .with_level(true) - .with_target(true) - .without_time() - .compact() - .fmt_fields({ - fmt::format::debug_fn(move |writer, field, value| { - if field.name() == "message" { - write!(writer, "{:?}", value)?; - } - Ok(()) - }) - }) - .with_filter(env_filter.clone()); - - // JSON file (capture everything) - let log_path = path.clone(); - let json_layer = fmt::layer() - .with_writer(move || { - fs::OpenOptions::new() - .create(true) - .append(true) - .open(&log_path) - .expect("open ~/.bourso/bourso.log") - }) - .json() - .with_target(true) - .with_level(true) - .flatten_event(true) - .with_filter(LevelFilter::TRACE); - - tracing_subscriber::registry() - .with(console_layer) - .with(json_layer) - .init(); - - Ok(()) -} diff --git a/src/settings/mod.rs b/src/settings/mod.rs new file mode 100644 index 0000000..929419e --- /dev/null +++ b/src/settings/mod.rs @@ -0,0 +1,83 @@ +use anyhow::{anyhow, Context, Result}; +use directories::ProjectDirs; +use serde::{Deserialize, Serialize}; +use serde_json::{from_str, to_string_pretty}; +use std::{fs, path::PathBuf}; + +#[derive(Serialize, Deserialize, Default)] +pub struct Settings { + #[serde(rename = "clientNumber")] + pub client_number: Option, + #[serde(rename = "password")] + pub password: Option, +} + +pub trait SettingsStore { + fn load(&self) -> Result; + fn save(&self, settings: &Settings) -> Result<()>; +} + +pub struct FileSettingsStore { + directory: PathBuf, // injected base directory (e.g., platform config directory) + file: &'static str, // e.g., "settings.json" +} + +impl FileSettingsStore { + pub fn new(directory: PathBuf) -> Self { + Self { + directory, + file: "settings.json", + } + } + + /// Convenience: build from ProjectDirs config directory. + /// `qualifier` can be "" if you don’t have one. Example: + /// - Windows: %APPDATA%\\\\settings.json + /// - macOS: ~/Library/Application Support//settings.json + /// - Linux: ~/.config//settings.json + pub fn from_project_dirs( + qualifier: &str, + organization: &str, + application: &str, + ) -> Result { + let project_directories = ProjectDirs::from(qualifier, organization, application) + .ok_or_else(|| anyhow!("Could not determine project directories"))?; + Ok(Self::new(project_directories.config_dir().to_path_buf())) + } + + fn path(&self) -> PathBuf { + self.directory.join(self.file) + } +} + +impl SettingsStore for FileSettingsStore { + fn load(&self) -> Result { + fs::create_dir_all(&self.directory).with_context(|| { + format!( + "Failed to create settings directory: {}", + self.directory.display() + ) + })?; + let path = self.path(); + let content = match fs::read_to_string(&path) { + Ok(content) => content, + Err(_) => { + let defaults = Settings::default(); + self.save(&defaults)?; + return Ok(defaults); + } + }; + from_str(&content).context("Failed to deserialize settings") + } + + fn save(&self, settings: &Settings) -> Result<()> { + fs::create_dir_all(&self.directory).with_context(|| { + format!( + "Failed to create settings directory: {}", + self.directory.display() + ) + })?; + fs::write(&self.path(), to_string_pretty(settings)?) + .with_context(|| format!("Failed to persist settings file: {}", self.path().display())) + } +} From 5a3daf8c6ffa33598e76165209edb797a5a176a1 Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Tue, 4 Nov 2025 02:52:51 +0000 Subject: [PATCH 02/49] refactor: reorganize settings management into a dedicated module and streamline file storage implementation --- src/settings/mod.rs | 86 ++-------------------------------------- src/settings/settings.rs | 83 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 82 deletions(-) create mode 100644 src/settings/settings.rs diff --git a/src/settings/mod.rs b/src/settings/mod.rs index 929419e..efdc601 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -1,83 +1,5 @@ -use anyhow::{anyhow, Context, Result}; -use directories::ProjectDirs; -use serde::{Deserialize, Serialize}; -use serde_json::{from_str, to_string_pretty}; -use std::{fs, path::PathBuf}; +mod logging; +mod settings; -#[derive(Serialize, Deserialize, Default)] -pub struct Settings { - #[serde(rename = "clientNumber")] - pub client_number: Option, - #[serde(rename = "password")] - pub password: Option, -} - -pub trait SettingsStore { - fn load(&self) -> Result; - fn save(&self, settings: &Settings) -> Result<()>; -} - -pub struct FileSettingsStore { - directory: PathBuf, // injected base directory (e.g., platform config directory) - file: &'static str, // e.g., "settings.json" -} - -impl FileSettingsStore { - pub fn new(directory: PathBuf) -> Self { - Self { - directory, - file: "settings.json", - } - } - - /// Convenience: build from ProjectDirs config directory. - /// `qualifier` can be "" if you don’t have one. Example: - /// - Windows: %APPDATA%\\\\settings.json - /// - macOS: ~/Library/Application Support//settings.json - /// - Linux: ~/.config//settings.json - pub fn from_project_dirs( - qualifier: &str, - organization: &str, - application: &str, - ) -> Result { - let project_directories = ProjectDirs::from(qualifier, organization, application) - .ok_or_else(|| anyhow!("Could not determine project directories"))?; - Ok(Self::new(project_directories.config_dir().to_path_buf())) - } - - fn path(&self) -> PathBuf { - self.directory.join(self.file) - } -} - -impl SettingsStore for FileSettingsStore { - fn load(&self) -> Result { - fs::create_dir_all(&self.directory).with_context(|| { - format!( - "Failed to create settings directory: {}", - self.directory.display() - ) - })?; - let path = self.path(); - let content = match fs::read_to_string(&path) { - Ok(content) => content, - Err(_) => { - let defaults = Settings::default(); - self.save(&defaults)?; - return Ok(defaults); - } - }; - from_str(&content).context("Failed to deserialize settings") - } - - fn save(&self, settings: &Settings) -> Result<()> { - fs::create_dir_all(&self.directory).with_context(|| { - format!( - "Failed to create settings directory: {}", - self.directory.display() - ) - })?; - fs::write(&self.path(), to_string_pretty(settings)?) - .with_context(|| format!("Failed to persist settings file: {}", self.path().display())) - } -} +pub use logging::LoggerBuilder; +pub use settings::{FileSettingsStore, Settings, SettingsStore}; diff --git a/src/settings/settings.rs b/src/settings/settings.rs new file mode 100644 index 0000000..929419e --- /dev/null +++ b/src/settings/settings.rs @@ -0,0 +1,83 @@ +use anyhow::{anyhow, Context, Result}; +use directories::ProjectDirs; +use serde::{Deserialize, Serialize}; +use serde_json::{from_str, to_string_pretty}; +use std::{fs, path::PathBuf}; + +#[derive(Serialize, Deserialize, Default)] +pub struct Settings { + #[serde(rename = "clientNumber")] + pub client_number: Option, + #[serde(rename = "password")] + pub password: Option, +} + +pub trait SettingsStore { + fn load(&self) -> Result; + fn save(&self, settings: &Settings) -> Result<()>; +} + +pub struct FileSettingsStore { + directory: PathBuf, // injected base directory (e.g., platform config directory) + file: &'static str, // e.g., "settings.json" +} + +impl FileSettingsStore { + pub fn new(directory: PathBuf) -> Self { + Self { + directory, + file: "settings.json", + } + } + + /// Convenience: build from ProjectDirs config directory. + /// `qualifier` can be "" if you don’t have one. Example: + /// - Windows: %APPDATA%\\\\settings.json + /// - macOS: ~/Library/Application Support//settings.json + /// - Linux: ~/.config//settings.json + pub fn from_project_dirs( + qualifier: &str, + organization: &str, + application: &str, + ) -> Result { + let project_directories = ProjectDirs::from(qualifier, organization, application) + .ok_or_else(|| anyhow!("Could not determine project directories"))?; + Ok(Self::new(project_directories.config_dir().to_path_buf())) + } + + fn path(&self) -> PathBuf { + self.directory.join(self.file) + } +} + +impl SettingsStore for FileSettingsStore { + fn load(&self) -> Result { + fs::create_dir_all(&self.directory).with_context(|| { + format!( + "Failed to create settings directory: {}", + self.directory.display() + ) + })?; + let path = self.path(); + let content = match fs::read_to_string(&path) { + Ok(content) => content, + Err(_) => { + let defaults = Settings::default(); + self.save(&defaults)?; + return Ok(defaults); + } + }; + from_str(&content).context("Failed to deserialize settings") + } + + fn save(&self, settings: &Settings) -> Result<()> { + fs::create_dir_all(&self.directory).with_context(|| { + format!( + "Failed to create settings directory: {}", + self.directory.display() + ) + })?; + fs::write(&self.path(), to_string_pretty(settings)?) + .with_context(|| format!("Failed to persist settings file: {}", self.path().display())) + } +} From 39825d0adf43448b07ffc718e2c5a1b4cf9422f0 Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Tue, 4 Nov 2025 03:04:11 +0000 Subject: [PATCH 03/49] refactor: simplify FileSettingsStore initialization and enhance project directory handling --- src/settings/settings.rs | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/src/settings/settings.rs b/src/settings/settings.rs index 929419e..4b149a0 100644 --- a/src/settings/settings.rs +++ b/src/settings/settings.rs @@ -4,6 +4,11 @@ use serde::{Deserialize, Serialize}; use serde_json::{from_str, to_string_pretty}; use std::{fs, path::PathBuf}; +const SETTINGS_QUALIFIER: &str = ""; +const SETTINGS_ORGANIZATION: &str = "bourso"; +const SETTINGS_APPLICATION: &str = "bourso-cli"; +const SETTINGS_FILE: &str = "settings.json"; + #[derive(Serialize, Deserialize, Default)] pub struct Settings { #[serde(rename = "clientNumber")] @@ -18,31 +23,27 @@ pub trait SettingsStore { } pub struct FileSettingsStore { - directory: PathBuf, // injected base directory (e.g., platform config directory) - file: &'static str, // e.g., "settings.json" + directory: PathBuf, // platform config directory (from ProjectDirs) + file: &'static str, // "settings.json" } impl FileSettingsStore { - pub fn new(directory: PathBuf) -> Self { - Self { - directory, - file: "settings.json", - } - } - - /// Convenience: build from ProjectDirs config directory. - /// `qualifier` can be "" if you don’t have one. Example: + /// Build from ProjectDirs config directory: /// - Windows: %APPDATA%\\\\settings.json /// - macOS: ~/Library/Application Support//settings.json /// - Linux: ~/.config//settings.json - pub fn from_project_dirs( - qualifier: &str, - organization: &str, - application: &str, - ) -> Result { - let project_directories = ProjectDirs::from(qualifier, organization, application) - .ok_or_else(|| anyhow!("Could not determine project directories"))?; - Ok(Self::new(project_directories.config_dir().to_path_buf())) + pub fn new() -> Result { + let project_dirs = ProjectDirs::from( + SETTINGS_QUALIFIER, + SETTINGS_ORGANIZATION, + SETTINGS_APPLICATION, + ) + .ok_or_else(|| anyhow!("Could not determine project directories"))?; + + Ok(Self { + directory: project_dirs.config_dir().to_path_buf(), + file: SETTINGS_FILE, + }) } fn path(&self) -> PathBuf { From d795ae211957699f73944c8a8d33fb21b0abf1db Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Tue, 4 Nov 2025 03:12:25 +0000 Subject: [PATCH 04/49] refactor: introduce constants for application settings and update settings module to use them --- src/settings/consts.rs | 6 ++++++ src/settings/mod.rs | 1 + src/settings/settings.rs | 13 +++---------- 3 files changed, 10 insertions(+), 10 deletions(-) create mode 100644 src/settings/consts.rs diff --git a/src/settings/consts.rs b/src/settings/consts.rs new file mode 100644 index 0000000..7ae6d99 --- /dev/null +++ b/src/settings/consts.rs @@ -0,0 +1,6 @@ +pub const APP_QUALIFIER: &str = ""; +pub const APP_ORGANIZATION: &str = "bourso"; +pub const APP_NAME: &str = "bourso-cli"; + +pub const SETTINGS_FILE: &str = "settings.json"; +pub const LOG_FILE: &str = "bourso.log"; diff --git a/src/settings/mod.rs b/src/settings/mod.rs index efdc601..916299a 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -1,3 +1,4 @@ +mod consts; mod logging; mod settings; diff --git a/src/settings/settings.rs b/src/settings/settings.rs index 4b149a0..ece9770 100644 --- a/src/settings/settings.rs +++ b/src/settings/settings.rs @@ -4,10 +4,7 @@ use serde::{Deserialize, Serialize}; use serde_json::{from_str, to_string_pretty}; use std::{fs, path::PathBuf}; -const SETTINGS_QUALIFIER: &str = ""; -const SETTINGS_ORGANIZATION: &str = "bourso"; -const SETTINGS_APPLICATION: &str = "bourso-cli"; -const SETTINGS_FILE: &str = "settings.json"; +use crate::settings::consts::{APP_NAME, APP_ORGANIZATION, APP_QUALIFIER, SETTINGS_FILE}; #[derive(Serialize, Deserialize, Default)] pub struct Settings { @@ -33,12 +30,8 @@ impl FileSettingsStore { /// - macOS: ~/Library/Application Support//settings.json /// - Linux: ~/.config//settings.json pub fn new() -> Result { - let project_dirs = ProjectDirs::from( - SETTINGS_QUALIFIER, - SETTINGS_ORGANIZATION, - SETTINGS_APPLICATION, - ) - .ok_or_else(|| anyhow!("Could not determine project directories"))?; + let project_dirs = ProjectDirs::from(APP_QUALIFIER, APP_ORGANIZATION, APP_NAME) + .ok_or_else(|| anyhow!("Could not determine project directories"))?; Ok(Self { directory: project_dirs.config_dir().to_path_buf(), From 461dc1486a28b43f50ec3dae017a6b3415ee7ab3 Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Tue, 4 Nov 2025 03:45:34 +0000 Subject: [PATCH 05/49] feat: add logging configuration with tracing and tracing-appender support --- Cargo.lock | 34 +++++++++++++++++-- Cargo.toml | 1 + src/settings/consts.rs | 2 ++ src/settings/logging.rs | 72 ++++++++++++++++++++++++++++++++++++++++ src/settings/settings.rs | 4 --- 5 files changed, 106 insertions(+), 7 deletions(-) create mode 100644 src/settings/logging.rs diff --git a/Cargo.lock b/Cargo.lock index 473af06..10a3346 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -172,6 +172,7 @@ dependencies = [ "serde_json", "tokio", "tracing", + "tracing-appender", "tracing-subscriber", ] @@ -326,6 +327,21 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "deranged" version = "0.3.11" @@ -398,7 +414,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1309,7 +1325,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1581,7 +1597,7 @@ dependencies = [ "getrandom", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1755,6 +1771,18 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.30" diff --git a/Cargo.toml b/Cargo.toml index c1164b1..49a920f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ directories = { version = "5.0.1" } serde = { version = "1.0.189", features = ["derive"] } serde_json = { version = "1.0.107" } tracing = { version = "0.1.41" } +tracing-appender = { version = "0.2.3" } tracing-subscriber = { version = "0.3.20", features = ["fmt", "env-filter", "json"] } futures-util = { version = "0.3.31" } diff --git a/src/settings/consts.rs b/src/settings/consts.rs index 7ae6d99..5803a56 100644 --- a/src/settings/consts.rs +++ b/src/settings/consts.rs @@ -4,3 +4,5 @@ pub const APP_NAME: &str = "bourso-cli"; pub const SETTINGS_FILE: &str = "settings.json"; pub const LOG_FILE: &str = "bourso.log"; + +pub const DEFAULT_LOG_LEVEL: &str = "info"; diff --git a/src/settings/logging.rs b/src/settings/logging.rs new file mode 100644 index 0000000..ae7fc97 --- /dev/null +++ b/src/settings/logging.rs @@ -0,0 +1,72 @@ +use anyhow::{anyhow, Result}; +use directories::ProjectDirs; +use std::{ + fs, + io::{stderr, IsTerminal}, + path::PathBuf, +}; +use tracing_appender::rolling; +use tracing_subscriber::{ + filter::LevelFilter, + fmt::{self, format::debug_fn}, + prelude::*, + registry, EnvFilter, +}; + +use crate::settings::consts::{ + APP_NAME, APP_ORGANIZATION, APP_QUALIFIER, DEFAULT_LOG_LEVEL, LOG_FILE, +}; + +pub struct LoggerBuilder { + directory: PathBuf, // platform config directory (from ProjectDirs) + file: &'static str, // "bourso.log" +} + +impl LoggerBuilder { + pub fn new() -> Result { + let project_dirs = ProjectDirs::from(APP_QUALIFIER, APP_ORGANIZATION, APP_NAME) + .ok_or_else(|| anyhow!("Could not determine project directories"))?; + + Ok(Self { + directory: project_dirs.data_dir().to_path_buf(), + file: LOG_FILE, + }) + } + + pub fn init(self) -> Result<()> { + fs::create_dir_all(&self.directory)?; + + let env_filter = + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(DEFAULT_LOG_LEVEL)); + + let file_appender = rolling::never(&self.directory, &self.file); + + let console_layer = fmt::layer() + .with_writer(stderr) + .with_ansi(IsTerminal::is_terminal(&stderr())) + .with_level(true) + .without_time() + .compact() + .fmt_fields({ + debug_fn(move |writer, field, value| { + if field.name() == "message" { + write!(writer, "{:?}", value)?; + } + Ok(()) + }) + }) + .with_filter(env_filter); + + let json_layer = fmt::layer() + .json() + .with_writer(file_appender) + .with_target(true) + .with_level(true) + .flatten_event(true) + .with_filter(LevelFilter::TRACE); + + registry().with(console_layer).with(json_layer).init(); + + Ok(()) + } +} diff --git a/src/settings/settings.rs b/src/settings/settings.rs index ece9770..93b8410 100644 --- a/src/settings/settings.rs +++ b/src/settings/settings.rs @@ -25,10 +25,6 @@ pub struct FileSettingsStore { } impl FileSettingsStore { - /// Build from ProjectDirs config directory: - /// - Windows: %APPDATA%\\\\settings.json - /// - macOS: ~/Library/Application Support//settings.json - /// - Linux: ~/.config//settings.json pub fn new() -> Result { let project_dirs = ProjectDirs::from(APP_QUALIFIER, APP_ORGANIZATION, APP_NAME) .ok_or_else(|| anyhow!("Could not determine project directories"))?; From 53e3d9107d61fc6096cfadbcb5e8a18129b8679a Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Tue, 4 Nov 2025 03:57:08 +0000 Subject: [PATCH 06/49] refactor: replace LoggerBuilder with init_logger function for streamlined logging initialization --- src/settings/logging.rs | 84 +++++++++++++++++------------------------ src/settings/mod.rs | 2 +- 2 files changed, 36 insertions(+), 50 deletions(-) diff --git a/src/settings/logging.rs b/src/settings/logging.rs index ae7fc97..2cb89f1 100644 --- a/src/settings/logging.rs +++ b/src/settings/logging.rs @@ -3,7 +3,6 @@ use directories::ProjectDirs; use std::{ fs, io::{stderr, IsTerminal}, - path::PathBuf, }; use tracing_appender::rolling; use tracing_subscriber::{ @@ -17,56 +16,43 @@ use crate::settings::consts::{ APP_NAME, APP_ORGANIZATION, APP_QUALIFIER, DEFAULT_LOG_LEVEL, LOG_FILE, }; -pub struct LoggerBuilder { - directory: PathBuf, // platform config directory (from ProjectDirs) - file: &'static str, // "bourso.log" -} - -impl LoggerBuilder { - pub fn new() -> Result { - let project_dirs = ProjectDirs::from(APP_QUALIFIER, APP_ORGANIZATION, APP_NAME) - .ok_or_else(|| anyhow!("Could not determine project directories"))?; - - Ok(Self { - directory: project_dirs.data_dir().to_path_buf(), - file: LOG_FILE, - }) - } - - pub fn init(self) -> Result<()> { - fs::create_dir_all(&self.directory)?; - - let env_filter = - EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(DEFAULT_LOG_LEVEL)); - - let file_appender = rolling::never(&self.directory, &self.file); - - let console_layer = fmt::layer() - .with_writer(stderr) - .with_ansi(IsTerminal::is_terminal(&stderr())) - .with_level(true) - .without_time() - .compact() - .fmt_fields({ - debug_fn(move |writer, field, value| { - if field.name() == "message" { - write!(writer, "{:?}", value)?; - } - Ok(()) - }) +pub fn init_logger() -> Result<()> { + let project_dirs = ProjectDirs::from(APP_QUALIFIER, APP_ORGANIZATION, APP_NAME) + .ok_or_else(|| anyhow!("Could not determine project directories"))?; + + let directory = project_dirs.data_dir(); + fs::create_dir_all(directory)?; + + let env_filter = + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(DEFAULT_LOG_LEVEL)); + + let file_appender = rolling::never(directory, LOG_FILE); + + let console_layer = fmt::layer() + .with_writer(stderr) + .with_ansi(IsTerminal::is_terminal(&stderr())) + .with_level(true) + .without_time() + .compact() + .fmt_fields({ + debug_fn(move |writer, field, value| { + if field.name() == "message" { + write!(writer, "{:?}", value)?; + } + Ok(()) }) - .with_filter(env_filter); + }) + .with_filter(env_filter); - let json_layer = fmt::layer() - .json() - .with_writer(file_appender) - .with_target(true) - .with_level(true) - .flatten_event(true) - .with_filter(LevelFilter::TRACE); + let json_layer = fmt::layer() + .json() + .with_writer(file_appender) + .with_target(true) + .with_level(true) + .flatten_event(true) + .with_filter(LevelFilter::TRACE); - registry().with(console_layer).with(json_layer).init(); + registry().with(console_layer).with(json_layer).init(); - Ok(()) - } + Ok(()) } diff --git a/src/settings/mod.rs b/src/settings/mod.rs index 916299a..5446cfe 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -2,5 +2,5 @@ mod consts; mod logging; mod settings; -pub use logging::LoggerBuilder; +pub use logging::init_logger; pub use settings::{FileSettingsStore, Settings, SettingsStore}; From a24323bc44662075d95093e63fae4c98500e0a6b Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Thu, 6 Nov 2025 03:01:55 +0000 Subject: [PATCH 07/49] feat: implement AuthService with login and MFA handling, and add CredentialsProvider and ClientFactory traits --- src/services/auth.rs | 142 +++++++++++++++++++++++++++++++++++++++++++ src/services/mod.rs | 5 ++ 2 files changed, 147 insertions(+) create mode 100644 src/services/auth.rs create mode 100644 src/services/mod.rs diff --git a/src/services/auth.rs b/src/services/auth.rs new file mode 100644 index 0000000..637aa5e --- /dev/null +++ b/src/services/auth.rs @@ -0,0 +1,142 @@ +use std::sync::Arc; + +use anyhow::{Context, Result}; +use bourso_api::client::{error::ClientError, BoursoWebClient}; +use tracing::{info, warn}; + +use crate::settings::SettingsStore; + +// TODO: fix naming, too many mismatches with customer_id / username / client_id / client_number +// TODO: does it make sense to have MFA handling in the CLI? + +pub trait CredentialsProvider: Send + Sync { + fn read_password(&self, prompt: &str) -> Result; +} + +pub trait ClientFactory: Send + Sync { + fn new_client(&self) -> BoursoWebClient; +} + +pub struct AuthService { + settings_store: Arc, + credentials_provider: Arc, + client_factory: Arc, +} + +impl AuthService { + pub fn new( + settings_store: Arc, + credentials_provider: Arc, + client_factory: Arc, + ) -> Self { + Self { + settings_store, + credentials_provider, + client_factory, + } + } + + pub fn with_defaults(store: Arc) -> Self { + Self::new( + store, + Arc::new(StdinCredentialsProvider), + Arc::new(DefaultClientFactory), + ) + } + + pub async fn login(&self) -> Result> { + let settings = self.settings_store.load()?; + let Some(client_number) = settings.client_number else { + warn!("No client number found in settings, please run `bourso config` to set it"); + return Ok(None); + }; + + info!("We'll try to log you in with your customer id: {client_number}"); + info!("If you want to change it, you can run `bourso config` to set it"); + println!(); + + let password = match settings.password { + Some(password) => password, + None => { + info!("We'll need your password to log you in. It will not be stored."); + self.credentials_provider + .read_password("Enter your password (hidden):") + .context("Failed to read password")? + .trim() + .to_string() + } + }; + + let mut client = self.client_factory.new_client(); + client.init_session().await?; + match client.login(&client_number, &password).await { + Ok(_) => { + info!("Login successful ✅"); + Ok(Some(client)) + } + Err(e) => { + if let Some(ClientError::MfaRequired) = e.downcast_ref::() { + self.handle_mfa(client, &client_number, &password).await + } else { + Err(e) + } + } + } + } + + async fn handle_mfa( + &self, + mut client: BoursoWebClient, + client_number: &str, + password: &str, + ) -> Result> { + let mut mfa_count = 0usize; + loop { + if mfa_count == 2 { + warn!("MFA threshold reached. Reinitializing session and logging in again."); + client.init_session().await?; + client.login(client_number, password).await?; + info!("Login successful ✅"); + return Ok(Some(client)); + } + + let (otp_id, token_form, mfa_type) = client.request_mfa().await?; + let code = self + .credentials_provider + .read_password("Enter your MFA code (hidden):") + .context("Failed to read MFA code")? + .trim() + .to_string(); + + match client.submit_mfa(mfa_type, otp_id, code, token_form).await { + Ok(_) => { + info!("MFA successfully submitted ✅"); + return Ok(Some(client)); + } + Err(e) => { + if let Some(ClientError::MfaRequired) = e.downcast_ref::() { + mfa_count += 1; + continue; + } else { + return Err(e); + } + } + } + } + } +} + +pub struct StdinCredentialsProvider; +impl CredentialsProvider for StdinCredentialsProvider { + fn read_password(&self, prompt: &str) -> Result { + println!("{prompt}"); + Ok(rpassword::read_password()?) + } +} + +pub struct DefaultClientFactory; +impl ClientFactory for DefaultClientFactory { + fn new_client(&self) -> BoursoWebClient { + bourso_api::get_client() + } +} diff --git a/src/services/mod.rs b/src/services/mod.rs new file mode 100644 index 0000000..89b8635 --- /dev/null +++ b/src/services/mod.rs @@ -0,0 +1,5 @@ +pub mod auth; + +pub use auth::{ + AuthService, ClientFactory, CredentialsProvider, DefaultClientFactory, StdinCredentialsProvider, +}; From f9ea1e2e4fac4a87c67db19b0c1c3e9bb1758ab9 Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Thu, 6 Nov 2025 03:27:25 +0000 Subject: [PATCH 08/49] refactor: rename settings module to store and implement FileSettingsStore for file-based settings management --- src/settings/mod.rs | 4 ++-- src/settings/{settings.rs => store.rs} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/settings/{settings.rs => store.rs} (97%) diff --git a/src/settings/mod.rs b/src/settings/mod.rs index 5446cfe..080bfea 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -1,6 +1,6 @@ mod consts; mod logging; -mod settings; +mod store; pub use logging::init_logger; -pub use settings::{FileSettingsStore, Settings, SettingsStore}; +pub use store::{FileSettingsStore, Settings, SettingsStore}; diff --git a/src/settings/settings.rs b/src/settings/store.rs similarity index 97% rename from src/settings/settings.rs rename to src/settings/store.rs index 93b8410..4013f2e 100644 --- a/src/settings/settings.rs +++ b/src/settings/store.rs @@ -67,7 +67,7 @@ impl SettingsStore for FileSettingsStore { self.directory.display() ) })?; - fs::write(&self.path(), to_string_pretty(settings)?) + fs::write(self.path(), to_string_pretty(settings)?) .with_context(|| format!("Failed to persist settings file: {}", self.path().display())) } } From fa3f1ce0af774c71d4c9b37f522078b42463ef2a Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Thu, 6 Nov 2025 03:27:46 +0000 Subject: [PATCH 09/49] feat: implement account management, configuration saving, and transfer handling in commands module --- src/commands/accounts.rs | 34 +++++++++++++++++++ src/commands/config.rs | 14 ++++++++ src/commands/mod.rs | 5 +++ src/commands/quote.rs | 44 +++++++++++++++++++++++++ src/commands/trade/mod.rs | 11 +++++++ src/commands/trade/order.rs | 48 +++++++++++++++++++++++++++ src/commands/transfer.rs | 66 +++++++++++++++++++++++++++++++++++++ 7 files changed, 222 insertions(+) create mode 100644 src/commands/accounts.rs create mode 100644 src/commands/config.rs create mode 100644 src/commands/mod.rs create mode 100644 src/commands/quote.rs create mode 100644 src/commands/trade/mod.rs create mode 100644 src/commands/trade/order.rs create mode 100644 src/commands/transfer.rs diff --git a/src/commands/accounts.rs b/src/commands/accounts.rs new file mode 100644 index 0000000..42a5801 --- /dev/null +++ b/src/commands/accounts.rs @@ -0,0 +1,34 @@ +use anyhow::Result; +use std::sync::Arc; +use tracing::info; + +use crate::cli::AccountsArgs; +use crate::services::AuthService; +use crate::settings::FileSettingsStore; +use bourso_api::account::{Account, AccountKind}; + +pub async fn handle(args: AccountsArgs) -> Result<()> { + let settings_store = Arc::new(FileSettingsStore::new()?); + let auth_service = AuthService::with_defaults(settings_store); + + let Some(client) = auth_service.login().await? else { + return Ok(()); + }; + + let kind = if args.banking { + Some(AccountKind::Banking) + } else if args.saving { + Some(AccountKind::Savings) + } else if args.trading { + Some(AccountKind::Trading) + } else if args.loans { + Some(AccountKind::Loans) + } else { + None + }; + + let accounts: Vec = client.get_accounts(kind).await?; + info!("Found {} accounts", accounts.len()); + println!("{:#?}", accounts); + Ok(()) +} diff --git a/src/commands/config.rs b/src/commands/config.rs new file mode 100644 index 0000000..930f326 --- /dev/null +++ b/src/commands/config.rs @@ -0,0 +1,14 @@ +use anyhow::Result; +use tracing::info; + +use crate::cli::ConfigArgs; +use crate::settings::{FileSettingsStore, Settings, SettingsStore}; + +pub async fn handle(args: ConfigArgs) -> Result<()> { + FileSettingsStore::new()?.save(&Settings { + client_number: Some(args.client_number), + password: None, + })?; + info!("Configuration saved successfully ✅"); + Ok(()) +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 0000000..9b6829e --- /dev/null +++ b/src/commands/mod.rs @@ -0,0 +1,5 @@ +pub mod accounts; +pub mod config; +pub mod quote; +pub mod trade; +pub mod transfer; diff --git a/src/commands/quote.rs b/src/commands/quote.rs new file mode 100644 index 0000000..07cdc66 --- /dev/null +++ b/src/commands/quote.rs @@ -0,0 +1,44 @@ +use anyhow::Result; +use tracing::info; + +use crate::cli::{QuoteArgs, QuoteView}; + +pub async fn handle(args: QuoteArgs) -> Result<()> { + info!("Fetching quotes ..."); + + let client = bourso_api::get_client(); + let quotes = client + .get_ticks(&args.symbol, args.length, args.interval) + .await?; + + match args.view { + Some(QuoteView::Highest) => { + let highest = quotes.d.get_highest_value(); + info!(?highest, "Highest quote"); + } + Some(QuoteView::Lowest) => { + let lowest = quotes.d.get_lowest_value(); + info!(?lowest, "Lowest quote"); + } + Some(QuoteView::Average) => { + let average = quotes.d.get_average_value(); + info!(?average, "Average quote"); + } + Some(QuoteView::Volume) => { + let volume = quotes.d.get_volume(); + info!(?volume, "Volume"); + } + Some(QuoteView::Last) => { + let last = quotes.d.get_last_quote(); + info!(?last, "Last quote"); + } + None => { + info!("No view specified, displaying all quotes"); + for quote in quotes.d.get_quotes() { + info!(?quote, "Quote"); + } + } + } + + Ok(()) +} diff --git a/src/commands/trade/mod.rs b/src/commands/trade/mod.rs new file mode 100644 index 0000000..988c189 --- /dev/null +++ b/src/commands/trade/mod.rs @@ -0,0 +1,11 @@ +use anyhow::Result; + +use crate::cli::{TradeArgs, TradeCommands}; + +pub mod order; + +pub async fn handle(args: TradeArgs) -> Result<()> { + match args.command { + TradeCommands::Order(o) => order::handle(o).await, + } +} diff --git a/src/commands/trade/order.rs b/src/commands/trade/order.rs new file mode 100644 index 0000000..fba5288 --- /dev/null +++ b/src/commands/trade/order.rs @@ -0,0 +1,48 @@ +use anyhow::{Context, Result}; +use std::sync::Arc; +use tracing::{info, warn}; + +use crate::cli::{OrderArgs, OrderNewArgs, OrderSubcommands}; +use crate::services::AuthService; +use crate::settings::FileSettingsStore; +use bourso_api::account::AccountKind; +use bourso_api::client::trade::order::OrderSide; + +pub async fn handle(args: OrderArgs) -> Result<()> { + match args.command { + OrderSubcommands::New(n) => new_order(n).await, + OrderSubcommands::List(_) => { + warn!("Listing orders is coming soon."); + Ok(()) + } + OrderSubcommands::Cancel(_) => { + warn!("Cancel order is coming soon."); + Ok(()) + } + } +} + +async fn new_order(args: OrderNewArgs) -> Result<()> { + let store = Arc::new(FileSettingsStore::new()?); + let auth = AuthService::with_defaults(store); + + let Some(client) = auth.login().await? else { + return Ok(()); + }; + + // Choose a trading account and place the order + let accounts = client.get_accounts(Some(AccountKind::Trading)).await?; + let account = accounts + .iter() + .find(|a| a.id == args.account) + .context("Account not found. Are you sure you have access to it? Run `bourso accounts` to list your accounts")?; + + let side: OrderSide = args.side; + let quantity: usize = args.quantity as usize; + let symbol = args.symbol; + + let _ = client.order(side, account, &symbol, quantity, None).await?; + + info!("Order submitted ✅"); + Ok(()) +} diff --git a/src/commands/transfer.rs b/src/commands/transfer.rs new file mode 100644 index 0000000..5aa9653 --- /dev/null +++ b/src/commands/transfer.rs @@ -0,0 +1,66 @@ +use anyhow::{Context, Result}; +use futures_util::{pin_mut, StreamExt}; +use std::sync::Arc; +use tracing::info; + +use crate::cli::TransferArgs; +use crate::services::AuthService; +use crate::settings::FileSettingsStore; +use bourso_api::client::transfer::TransferProgress; + +pub async fn handle(args: TransferArgs) -> Result<()> { + let settings_store = Arc::new(FileSettingsStore::new()?); + let auth_service = AuthService::with_defaults(settings_store); + + let Some(client) = auth_service.login().await? else { + return Ok(()); + }; + + let from_account_id = args.from_account; + let to_account_id = args.to_account; + let amount: f64 = args.amount.parse()?; + let reason = args.reason; + + let accounts = client.get_accounts(None).await?; + let from_account = accounts + .iter() + .find(|a| a.id == from_account_id) + .context("From account not found. Are you sure you have access to it? Run `bourso accounts` to list your accounts")?; + let to_account = accounts + .iter() + .find(|a| a.id == to_account_id) + .context("To account not found. Are you sure you have access to it? Run `bourso accounts` to list your accounts")?; + + let stream = client.transfer_funds(amount, from_account.clone(), to_account.clone(), reason); + + pin_mut!(stream); + while let Some(progress_result) = stream.next().await { + let progress = progress_result?; + let step = progress.step_number(); + let total = TransferProgress::total_steps(); + let percentage = (step as f32 / total as f32 * 100.0) as u8; + + let bar_length = 30usize; + let filled = (bar_length as f32 * step as f32 / total as f32) as usize; + let bar: String = "█".repeat(filled) + &"░".repeat(bar_length - filled); + + print!( + "\x1B[2K\r[{}] {:3}% - {}/{} - {}", + bar, + percentage, + step, + total, + progress.description() + ); + use std::io::Write; + std::io::stdout().flush().unwrap(); + } + println!(); + + info!( + "Transfer of {} from account {} to account {} successful ✅", + amount, from_account.id, to_account.id + ); + + Ok(()) +} From 1e6238ae0194632de7129531953895fad10833e2 Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Thu, 6 Nov 2025 03:27:56 +0000 Subject: [PATCH 10/49] refactor: update CLI argument names and types for improved clarity and validation --- src/cli.rs | 14 +-- src/lib.rs | 348 ++-------------------------------------------------- src/main.rs | 13 +- 3 files changed, 25 insertions(+), 350 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 875d752..4d5c5da 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -2,7 +2,7 @@ use bourso_api::client::trade::order::OrderSide; use clap::{value_parser, Args, Parser, Subcommand}; // TODO: add debug option -// TODO: add type to fix primitive obsession (AccountId w/ FromStr impl) and value_parser +// TODO: add type to fix primitive obsession and value_parser (AccountId, QuoteInterval, QuoteLength, ...) #[derive(Parser)] #[command(version, author, about, long_about = None)] @@ -35,9 +35,9 @@ pub enum Commands { #[derive(Args)] pub struct ConfigArgs { - /// Your customer ID + /// Your client number #[arg(short, long, value_name = "ID")] - pub username: String, + pub client_number: String, } #[derive(Args)] @@ -124,13 +124,13 @@ pub struct QuoteArgs { #[arg( long, default_value = "30", - value_parser = ["1","5","30","90","180","365","1825","3650"] + value_parser = value_parser!(i64).range(1..=3650) )] - pub length: String, + pub length: i64, /// Interval of the stock (use "0" for default) - #[arg(long, default_value = "0", value_parser = ["0"])] - pub interval: String, + #[arg(long, default_value = "0", value_parser = value_parser!(i64).range(0..))] + pub interval: i64, #[command(subcommand)] pub view: Option, diff --git a/src/lib.rs b/src/lib.rs index bd8466f..848c6ff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,343 +1,17 @@ -use anyhow::{Context, Result}; -use bourso_api::{ - account::{Account, AccountKind}, - client::{ - trade::{order::OrderSide, tick::QuoteTab}, - transfer::TransferProgress, - BoursoWebClient, - }, - get_client, -}; -use clap::ArgMatches; -use futures_util::{pin_mut, StreamExt}; -use tracing::{debug, info, warn}; +use anyhow::Result; pub mod cli; +pub mod commands; +pub mod services; pub mod settings; -use settings::{get_settings, save_settings, Settings}; - -#[cfg(not(tarpaulin_include))] -pub async fn parse_matches(matches: ArgMatches) -> Result<()> { - let settings = match matches.get_one::("credentials") { - Some(credentials_path) => Settings::load(credentials_path)?, - None => get_settings()?, - }; - - info!("Welcome to BoursoBank CLI 👋"); - info!( - "ℹ️ - Version {}. Make sure you're running the latest version: {}", - env!("CARGO_PKG_VERSION"), - env!("CARGO_PKG_REPOSITORY") - ); - println!(""); - - match matches.subcommand() { - // These matches do not require authentication - Some(("config", config_matches)) => { - let customer_id = config_matches - .get_one::("username") - .map(|s| s.as_str()) - .unwrap(); - save_settings(&Settings { - customer_id: Some(customer_id.to_string()), - password: None, - })?; - info!("Configuration saved ✅"); - return Ok(()); - } - Some(("quote", quote_matches)) => { - info!("Fetching quotes..."); - - let symbol = quote_matches - .get_one::("symbol") - .map(|s| s.as_str()) - .unwrap(); - let length = quote_matches - .get_one::("length") - .map(|s| s.as_str()) - .unwrap(); - let interval = quote_matches - .get_one::("interval") - .map(|s| s.as_str()) - .unwrap(); - let web_client: BoursoWebClient = get_client(); - - let quotes = web_client - .get_ticks(symbol, length.parse()?, interval.parse()?) - .await?; - - match quote_matches.subcommand() { - Some(("highest", _)) => { - let highest_quote = quotes.d.get_highest_value(); - info!(highest_quote, "Highest quote: {:#?}", highest_quote); - } - Some(("lowest", _)) => { - let lowest_quote = quotes.d.get_lowest_value(); - info!(lowest_quote, "Lowest quote: {:#?}", lowest_quote); - } - Some(("volume", _)) => { - let volume = quotes.d.get_volume(); - info!(volume, "Volume: {:#?}", volume); - } - Some(("average", _)) => { - let average_quote = quotes.d.get_average_value(); - info!(average_quote, "Average quote: {:#?}", average_quote); - } - Some(("last", _)) => { - let quote: QuoteTab; - - let last_quote = quotes.d.get_last_quote(); - if last_quote.is_some() { - quote = last_quote.unwrap(); - } else { - quote = quotes.d.quote_tab.last().unwrap().clone(); - } - - info!( - close = quote.close, open = quote.open, high = quote.high, low = quote.low, volume = quote.volume, - "Last quote: current: {:#?}, open: {:#?}, high: {:#?}, low: {:#?}, volume: {:#?}", - quote.close, quote.open, quote.high, quote.low, quote.volume - ); - } - _ => { - info!("Quotes:"); - for quote in quotes.d.quote_tab.iter() { - info!( - date = quote.date, close = quote.close, open = quote.open, high = quote.high, low = quote.low, volume = quote.volume, - "Quote day {:#?}: Close: {:#?}, Open: {:#?}, High: {:#?}, Low: {:#?}, Volume: {:#?}", - quote.date, quote.close, quote.open, quote.high, quote.low, quote.volume, - ); - } - } - } - - return Ok(()); - } - // These matches require authentication - Some(("accounts", _)) - | Some(("transactions", _)) - | Some(("balance", _)) - | Some(("trade", _)) - | Some(("transfer", _)) => (), - _ => unreachable!(), - } - - if settings.customer_id.is_none() { - warn!("Please configure your customer id with `bourso config --username `"); - return Ok(()); - } - let customer_id = settings.customer_id.unwrap(); - - info!( - "We'll try to log you in with your customer id: {}", - customer_id - ); - info!("If you want to change it, run `bourso config --username `"); - println!(""); - info!("We'll need your password to log you in. It will not be stored anywhere and will be asked everytime you run a command. The password will be hidden while typing."); - - // Get password from stdin - let password = match settings.password { - Some(password) => password, - None => rpassword::prompt_password("Enter your password: ") - .context("Failed to read password")? - .trim() - .to_string(), - }; - - let mut web_client: BoursoWebClient = get_client(); - web_client.init_session().await?; - match web_client.login(&customer_id, &password).await { - Ok(_) => { - info!("Login successful ✅"); - } - Err(e) => match e.downcast_ref() { - Some(bourso_api::client::error::ClientError::MfaRequired) => { - let mut mfa_required = true; - let mut mfa_count = 0; - while mfa_required { - // If MFA is passed twice, it means the user has passed an sms and email mfa - // which should clear the IP. We just need to reinitialize the session - // and login again to access the account. - if mfa_count == 2 { - warn!("MFA thresold reached. Trying to login again by reinitalizing the session."); - web_client = get_client(); - web_client.init_session().await?; - match web_client.login(&customer_id, &password).await { - Ok(_) => { - info!("Login successful ✅"); - break; - } - Err(e) => { - debug!("{:#?}", e); - return Err(e); - } - } - } - warn!("An MFA is required."); - - let (otp_id, token, mfa_type) = web_client.request_mfa().await?; - let code = rpassword::prompt_password("Enter your MFA code: ") - .context("Failed to read MFA code")? - .trim() - .to_string(); - match web_client.submit_mfa(mfa_type, otp_id, code, token).await { - Ok(_) => { - mfa_required = false; - } - Err(e) => match e.downcast_ref() { - Some(bourso_api::client::error::ClientError::MfaRequired) => { - mfa_count += 1; - } - _ => { - debug!("{:#?}", e); - return Err(e); - } - }, - } - } - - info!("MFA successful ✅"); - } - _ => { - debug!("{:#?}", e); - return Err(e); - } - }, - } - - let accounts: Vec; - - match matches.subcommand() { - Some(("accounts", sub_matches)) => { - if sub_matches.get_flag("banking") { - accounts = web_client.get_accounts(Some(AccountKind::Banking)).await?; - } else if sub_matches.get_flag("saving") { - accounts = web_client.get_accounts(Some(AccountKind::Savings)).await?; - } else if sub_matches.get_flag("trading") { - accounts = web_client.get_accounts(Some(AccountKind::Trading)).await?; - } else if sub_matches.get_flag("loans") { - accounts = web_client.get_accounts(Some(AccountKind::Loans)).await?; - } else { - accounts = web_client.get_accounts(None).await?; - } - - info!("Found {} accounts", accounts.len()); - println!("{:#?}", accounts); - } - - Some(("trade", trade_matches)) => { - accounts = web_client.get_accounts(Some(AccountKind::Trading)).await?; - - match trade_matches.subcommand() { - Some(("order", order_matches)) => { - match order_matches.subcommand() { - Some(("new", new_order_matches)) => { - let account_id = new_order_matches - .get_one::("account") - .map(|s| s.as_str()) - .unwrap(); - - // Get account from previously fetched accounts - let account = accounts - .iter() - .find(|a| a.id == account_id) - .context("Account not found. Are you sure you have access to it? Run `bourso accounts` to list your accounts")?; - - let side = new_order_matches.get_one::("side").unwrap(); - let quantity = new_order_matches.get_one::("quantity").unwrap(); - let symbol = new_order_matches - .get_one::("symbol") - .map(|s| s.as_str()) - .unwrap(); - - let _ = web_client - .order(side.to_owned(), account, symbol, quantity.to_owned(), None) - .await?; - } - _ => unreachable!(), - } - } - _ => unreachable!(), - } - } - - Some(("transfer", transfer_matches)) => { - accounts = web_client.get_accounts(None).await?; - - let from_account_id = transfer_matches - .get_one::("from_account") - .map(|s| s.as_str()) - .unwrap(); - let to_account_id = transfer_matches - .get_one::("to_account") - .map(|s| s.as_str()) - .unwrap(); - let amount = transfer_matches - .get_one::("amount") - .map(|s| s.parse::().unwrap()) - .unwrap(); - let reason = transfer_matches - .get_one::("reason") - .map(|s| s.as_str()); - - // Get from_account from previously fetched accounts - let from_account = accounts - .iter() - .find(|a| a.id == from_account_id) - .context("From account not found. Are you sure you have access to it? Run `bourso accounts` to list your accounts")?; - - // Get to_account from previously fetched accounts - let to_account = accounts - .iter() - .find(|a| a.id == to_account_id) - .context("To account not found. Are you sure you have access to it? Run `bourso accounts` to list your accounts")?; - - let stream = web_client.transfer_funds( - amount, - from_account.clone(), - to_account.clone(), - reason.map(|s| s.to_string()), - ); - - pin_mut!(stream); - - // Track progress and update display - while let Some(progress_result) = stream.next().await { - let progress = progress_result?; - let step = progress.step_number(); - let total = TransferProgress::total_steps(); - let percentage = (step as f32 / total as f32 * 100.0) as u8; - - // Create a simple progress bar - let bar_length = 30; - let filled = (bar_length as f32 * step as f32 / total as f32) as usize; - let bar: String = "█".repeat(filled) + &"░".repeat(bar_length - filled); - - // Use ANSI escape code to clear the line before printing - // \x1B[2K clears the entire line, \r returns cursor to start - print!( - "\x1B[2K\r[{}] {:3}% - {}/{} - {}", - bar, - percentage, - step, - total, - progress.description() - ); - use std::io::Write; - std::io::stdout().flush().unwrap(); - } - println!(); // New line after progress is complete - - info!( - "Transfer of {} from account {} to account {} successful ✅", - amount, from_account.id, to_account.id - ); - } - - _ => unreachable!(), +pub async fn run(cli: cli::Cli) -> Result<()> { + use cli::Commands::*; + match cli.command { + Config(args) => commands::config::handle(args).await, + Accounts(args) => commands::accounts::handle(args).await, + Trade(args) => commands::trade::handle(args).await, + Quote(args) => commands::quote::handle(args).await, + Transfer(args) => commands::transfer::handle(args).await, } - - Ok(()) } diff --git a/src/main.rs b/src/main.rs index 254b533..6148189 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,12 @@ use anyhow::Result; -use clap::CommandFactory; +use clap::Parser; + +use bourso_cli::settings::init_logger; #[tokio::main] async fn main() -> Result<()> { - bourso_cli::settings::init_logger()?; - - let matches = bourso_cli::cli::Cli::command().get_matches(); - - bourso_cli::parse_matches(matches).await + init_logger()?; + let cli = bourso_cli::cli::Cli::parse(); + bourso_cli::run(cli).await?; + Ok(()) } From d1ca91f48940a32fe67901acd1c2e1dbd2f5e0be Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Thu, 6 Nov 2025 03:28:03 +0000 Subject: [PATCH 11/49] refactor: clean up auth module imports and simplify mod.rs exports --- src/services/auth.rs | 5 ++--- src/services/mod.rs | 4 +--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/services/auth.rs b/src/services/auth.rs index 637aa5e..5bd6e08 100644 --- a/src/services/auth.rs +++ b/src/services/auth.rs @@ -1,10 +1,9 @@ -use std::sync::Arc; - use anyhow::{Context, Result}; -use bourso_api::client::{error::ClientError, BoursoWebClient}; +use std::sync::Arc; use tracing::{info, warn}; use crate::settings::SettingsStore; +use bourso_api::client::{error::ClientError, BoursoWebClient}; // TODO: fix naming, too many mismatches with customer_id / username / client_id / client_number // TODO: does it make sense to have MFA handling in the CLI? diff --git a/src/services/mod.rs b/src/services/mod.rs index 89b8635..b9325a5 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -1,5 +1,3 @@ pub mod auth; -pub use auth::{ - AuthService, ClientFactory, CredentialsProvider, DefaultClientFactory, StdinCredentialsProvider, -}; +pub use auth::AuthService; From b3a287527ae0ad9992f00096a73b422dab353af2 Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Thu, 6 Nov 2025 03:46:18 +0000 Subject: [PATCH 12/49] refactor: replace Arc with Box for settings store in commands and auth modules --- src/commands/accounts.rs | 4 ++-- src/commands/trade/order.rs | 4 ++-- src/commands/transfer.rs | 4 ++-- src/services/auth.rs | 47 +++++++++++++++++-------------------- 4 files changed, 28 insertions(+), 31 deletions(-) diff --git a/src/commands/accounts.rs b/src/commands/accounts.rs index 42a5801..40d1d06 100644 --- a/src/commands/accounts.rs +++ b/src/commands/accounts.rs @@ -1,14 +1,14 @@ use anyhow::Result; -use std::sync::Arc; use tracing::info; use crate::cli::AccountsArgs; use crate::services::AuthService; use crate::settings::FileSettingsStore; + use bourso_api::account::{Account, AccountKind}; pub async fn handle(args: AccountsArgs) -> Result<()> { - let settings_store = Arc::new(FileSettingsStore::new()?); + let settings_store = Box::new(FileSettingsStore::new()?); let auth_service = AuthService::with_defaults(settings_store); let Some(client) = auth_service.login().await? else { diff --git a/src/commands/trade/order.rs b/src/commands/trade/order.rs index fba5288..d3b4ea7 100644 --- a/src/commands/trade/order.rs +++ b/src/commands/trade/order.rs @@ -1,10 +1,10 @@ use anyhow::{Context, Result}; -use std::sync::Arc; use tracing::{info, warn}; use crate::cli::{OrderArgs, OrderNewArgs, OrderSubcommands}; use crate::services::AuthService; use crate::settings::FileSettingsStore; + use bourso_api::account::AccountKind; use bourso_api::client::trade::order::OrderSide; @@ -23,7 +23,7 @@ pub async fn handle(args: OrderArgs) -> Result<()> { } async fn new_order(args: OrderNewArgs) -> Result<()> { - let store = Arc::new(FileSettingsStore::new()?); + let store = Box::new(FileSettingsStore::new()?); let auth = AuthService::with_defaults(store); let Some(client) = auth.login().await? else { diff --git a/src/commands/transfer.rs b/src/commands/transfer.rs index 5aa9653..0157138 100644 --- a/src/commands/transfer.rs +++ b/src/commands/transfer.rs @@ -1,15 +1,15 @@ use anyhow::{Context, Result}; use futures_util::{pin_mut, StreamExt}; -use std::sync::Arc; use tracing::info; use crate::cli::TransferArgs; use crate::services::AuthService; use crate::settings::FileSettingsStore; + use bourso_api::client::transfer::TransferProgress; pub async fn handle(args: TransferArgs) -> Result<()> { - let settings_store = Arc::new(FileSettingsStore::new()?); + let settings_store = Box::new(FileSettingsStore::new()?); let auth_service = AuthService::with_defaults(settings_store); let Some(client) = auth_service.login().await? else { diff --git a/src/services/auth.rs b/src/services/auth.rs index 5bd6e08..c183308 100644 --- a/src/services/auth.rs +++ b/src/services/auth.rs @@ -1,5 +1,4 @@ use anyhow::{Context, Result}; -use std::sync::Arc; use tracing::{info, warn}; use crate::settings::SettingsStore; @@ -11,22 +10,35 @@ use bourso_api::client::{error::ClientError, BoursoWebClient}; pub trait CredentialsProvider: Send + Sync { fn read_password(&self, prompt: &str) -> Result; } +pub struct StdinCredentialsProvider; +impl CredentialsProvider for StdinCredentialsProvider { + fn read_password(&self, prompt: &str) -> Result { + println!("{prompt}"); + Ok(rpassword::read_password()?) + } +} pub trait ClientFactory: Send + Sync { fn new_client(&self) -> BoursoWebClient; } +pub struct DefaultClientFactory; +impl ClientFactory for DefaultClientFactory { + fn new_client(&self) -> BoursoWebClient { + bourso_api::get_client() + } +} pub struct AuthService { - settings_store: Arc, - credentials_provider: Arc, - client_factory: Arc, + settings_store: Box, + credentials_provider: Box, + client_factory: Box, } impl AuthService { pub fn new( - settings_store: Arc, - credentials_provider: Arc, - client_factory: Arc, + settings_store: Box, + credentials_provider: Box, + client_factory: Box, ) -> Self { Self { settings_store, @@ -35,11 +47,11 @@ impl AuthService { } } - pub fn with_defaults(store: Arc) -> Self { + pub fn with_defaults(store: Box) -> Self { Self::new( store, - Arc::new(StdinCredentialsProvider), - Arc::new(DefaultClientFactory), + Box::new(StdinCredentialsProvider), + Box::new(DefaultClientFactory), ) } @@ -124,18 +136,3 @@ impl AuthService { } } } - -pub struct StdinCredentialsProvider; -impl CredentialsProvider for StdinCredentialsProvider { - fn read_password(&self, prompt: &str) -> Result { - println!("{prompt}"); - Ok(rpassword::read_password()?) - } -} - -pub struct DefaultClientFactory; -impl ClientFactory for DefaultClientFactory { - fn new_client(&self) -> BoursoWebClient { - bourso_api::get_client() - } -} From 7748d4207ea84e7298dd360879ccbe8be2ad77b7 Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Thu, 6 Nov 2025 03:52:39 +0000 Subject: [PATCH 13/49] refactor: streamline module exports and update main function to use simplified imports --- src/lib.rs | 4 ++++ src/main.rs | 7 ++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 848c6ff..2e00c84 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,10 @@ pub mod commands; pub mod services; pub mod settings; +pub use services::AuthService; +pub use settings::init_logger; +pub use settings::{FileSettingsStore, Settings, SettingsStore}; + pub async fn run(cli: cli::Cli) -> Result<()> { use cli::Commands::*; match cli.command { diff --git a/src/main.rs b/src/main.rs index 6148189..45811e6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,13 @@ use anyhow::Result; use clap::Parser; -use bourso_cli::settings::init_logger; +use bourso_cli::cli::Cli; +use bourso_cli::{init_logger, run}; #[tokio::main] async fn main() -> Result<()> { init_logger()?; - let cli = bourso_cli::cli::Cli::parse(); - bourso_cli::run(cli).await?; + let cli = Cli::parse(); + run(cli).await?; Ok(()) } From 38654c8b1a65b832eed668ce6f3b77f70904ade2 Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Fri, 7 Nov 2025 03:37:42 +0000 Subject: [PATCH 14/49] feat: add TextProgressBar for enhanced transfer progress visualization in the transfer command --- src/commands/transfer.rs | 24 ++++++------------------ src/lib.rs | 2 ++ src/ux/mod.rs | 3 +++ src/ux/progress.rs | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 44 insertions(+), 18 deletions(-) create mode 100644 src/ux/mod.rs create mode 100644 src/ux/progress.rs diff --git a/src/commands/transfer.rs b/src/commands/transfer.rs index 0157138..6c7e5d1 100644 --- a/src/commands/transfer.rs +++ b/src/commands/transfer.rs @@ -5,6 +5,7 @@ use tracing::info; use crate::cli::TransferArgs; use crate::services::AuthService; use crate::settings::FileSettingsStore; +use crate::ux::progress::TextProgressBar; use bourso_api::client::transfer::TransferProgress; @@ -33,29 +34,16 @@ pub async fn handle(args: TransferArgs) -> Result<()> { let stream = client.transfer_funds(amount, from_account.clone(), to_account.clone(), reason); + let bar = TextProgressBar::new(30usize); pin_mut!(stream); while let Some(progress_result) = stream.next().await { let progress = progress_result?; - let step = progress.step_number(); - let total = TransferProgress::total_steps(); - let percentage = (step as f32 / total as f32 * 100.0) as u8; + let step = progress.step_number() as usize; + let total = TransferProgress::total_steps() as usize; - let bar_length = 30usize; - let filled = (bar_length as f32 * step as f32 / total as f32) as usize; - let bar: String = "█".repeat(filled) + &"░".repeat(bar_length - filled); - - print!( - "\x1B[2K\r[{}] {:3}% - {}/{} - {}", - bar, - percentage, - step, - total, - progress.description() - ); - use std::io::Write; - std::io::stdout().flush().unwrap(); + bar.render(step, total, progress.description()); } - println!(); + bar.finish(); info!( "Transfer of {} from account {} to account {} successful ✅", diff --git a/src/lib.rs b/src/lib.rs index 2e00c84..50144b8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,10 +4,12 @@ pub mod cli; pub mod commands; pub mod services; pub mod settings; +pub mod ux; pub use services::AuthService; pub use settings::init_logger; pub use settings::{FileSettingsStore, Settings, SettingsStore}; +pub use ux::TextProgressBar; pub async fn run(cli: cli::Cli) -> Result<()> { use cli::Commands::*; diff --git a/src/ux/mod.rs b/src/ux/mod.rs new file mode 100644 index 0000000..0f257a9 --- /dev/null +++ b/src/ux/mod.rs @@ -0,0 +1,3 @@ +pub mod progress; + +pub use progress::TextProgressBar; diff --git a/src/ux/progress.rs b/src/ux/progress.rs new file mode 100644 index 0000000..ee52d79 --- /dev/null +++ b/src/ux/progress.rs @@ -0,0 +1,33 @@ +use std::io::{stdout, Write}; + +pub struct TextProgressBar { + width: usize, +} + +impl TextProgressBar { + pub fn new(width: usize) -> Self { + Self { width } + } + + pub fn render(&self, step: usize, total: usize, description: &str) { + let (percentage, filled) = if total > 0 { + let percentage = (step as f32 / total as f32 * 100.0).clamp(0.0, 100.0); + let filled = ((self.width as f32) * (step as f32 / total as f32)) as usize; + (percentage, filled) + } else { + (0.0, 0usize) + }; + + let bar = format!("{}{}", "█".repeat(filled), "░".repeat(self.width - filled)); + + print!( + "\x1B[2K\r[{}] {:3.0}% - {}/{} - {}", + bar, percentage, step, total, description + ); + let _ = stdout().flush(); + } + + pub fn finish(&self) { + println!(); + } +} From 235a4fd9e263ba25c1253281910f189a54c72211 Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Fri, 7 Nov 2025 03:52:43 +0000 Subject: [PATCH 15/49] chore: update dependencies in Cargo.toml and Cargo.lock --- Cargo.lock | 1002 +++++++++++++++++++++++++++------------------------- Cargo.toml | 10 +- 2 files changed, 533 insertions(+), 479 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 10a3346..dc7fb64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,36 +2,15 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" - [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -43,9 +22,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -58,44 +37,44 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", - "once_cell", - "windows-sys 0.59.0", + "once_cell_polyfill", + "windows-sys 0.60.2", ] [[package]] name = "anyhow" -version = "1.0.95" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "async-stream" @@ -127,24 +106,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "backtrace" -version = "0.3.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", -] +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "base64" @@ -154,9 +118,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.7.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "bourso-cli" @@ -197,43 +161,43 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytes" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.9" +version = "1.2.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8293772165d9345bdaaa39b45b2109591e63fe5e6fbc23c6ff930a048aa310b" +checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" dependencies = [ + "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.39" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] @@ -272,15 +236,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "cookie" @@ -344,32 +308,32 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "deranged" -version = "0.3.11" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", ] [[package]] name = "directories" -version = "5.0.1" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -385,9 +349,9 @@ dependencies = [ [[package]] name = "document-features" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ "litrs", ] @@ -403,18 +367,18 @@ dependencies = [ [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -423,6 +387,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + [[package]] name = "fnv" version = "1.0.7" @@ -446,9 +416,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -507,9 +477,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", @@ -517,16 +487,22 @@ dependencies = [ ] [[package]] -name = "gimli" -version = "0.31.1" +name = "getrandom" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] [[package]] name = "h2" -version = "0.4.7" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", @@ -543,9 +519,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" [[package]] name = "heck" @@ -555,9 +531,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "http" -version = "1.2.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", @@ -576,12 +552,12 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", - "futures-util", + "futures-core", "http", "http-body", "pin-project-lite", @@ -589,25 +565,27 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.5" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.5.2" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "h2", "http", "http-body", "httparse", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -615,11 +593,10 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.3" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "futures-util", "http", "hyper", "hyper-util", @@ -648,33 +625,41 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.10" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ + "base64", "bytes", "futures-channel", + "futures-core", "futures-util", "http", "http-body", "hyper", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] name = "iana-time-zone" -version = "0.1.61" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", "windows-core", ] @@ -690,21 +675,22 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", + "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -713,104 +699,66 @@ dependencies = [ "zerovec", ] -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" dependencies = [ - "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", + "icu_locale_core", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -819,9 +767,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -829,9 +777,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", "hashbrown", @@ -839,27 +787,37 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.10.1" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +dependencies = [ + "memchr", + "serde", +] [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" dependencies = [ "once_cell", "wasm-bindgen", @@ -873,15 +831,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.169" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ "bitflags", "libc", @@ -889,29 +847,28 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.15" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" -version = "0.7.4" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "litrs" -version = "0.4.1" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] @@ -932,9 +889,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "mime" @@ -952,31 +909,22 @@ dependencies = [ "unicase", ] -[[package]] -name = "miniz_oxide" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" -dependencies = [ - "adler2", -] - [[package]] name = "mio" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", "wasi", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "native-tls" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" dependencies = [ "libc", "log", @@ -995,7 +943,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1014,25 +962,22 @@ dependencies = [ ] [[package]] -name = "object" -version = "0.36.7" +name = "once_cell" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] -name = "once_cell" -version = "1.20.2" +name = "once_cell_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "openssl" -version = "0.10.68" +version = "0.10.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" dependencies = [ "bitflags", "cfg-if", @@ -1056,15 +1001,15 @@ dependencies = [ [[package]] name = "openssl-probe" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.104" +version = "0.9.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2" dependencies = [ "cc", "libc", @@ -1080,9 +1025,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -1090,22 +1035,22 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" @@ -1121,9 +1066,18 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.31" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "potential_utf" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] [[package]] name = "powerfmt" @@ -1133,9 +1087,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "proc-macro2" -version = "1.0.93" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] @@ -1158,38 +1112,44 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.38" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "redox_syscall" -version = "0.5.8" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags", ] [[package]] name = "redox_users" -version = "0.4.6" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom", + "getrandom 0.2.16", "libredox", - "thiserror", + "thiserror 2.0.17", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", @@ -1199,9 +1159,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", @@ -1210,15 +1170,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.12.12" +version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ "base64", "bytes", @@ -1235,37 +1195,34 @@ dependencies = [ "hyper-rustls", "hyper-tls", "hyper-util", - "ipnet", "js-sys", "log", "mime", "mime_guess", "native-tls", - "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", - "system-configuration", "tokio", "tokio-native-tls", "tower", + "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows-registry", ] [[package]] name = "reqwest_cookie_store" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0b36498c7452f11b1833900f31fbb01fc46be20992a50269c88cf59d79f54e9" +checksum = "2314c325724fea278d44c13a525ebf60074e33c05f13b4345c076eb65b2446b3" dependencies = [ "bytes", "cookie_store", @@ -1275,64 +1232,57 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.8" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.16", "libc", - "spin", "untrusted", "windows-sys 0.52.0", ] [[package]] name = "rpassword" -version = "7.3.1" +version = "7.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f" +checksum = "66d4c8b64f049c6721ec8ccec37ddfc3d641c4a7fca57e8f2a89de509c73df39" dependencies = [ "libc", "rtoolbox", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] name = "rtoolbox" -version = "0.0.2" +version = "0.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e" +checksum = "a7cc970b249fbe527d6e02e0a227762c9108b2f49d81094fe357ffc6d14d7f6f" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - [[package]] name = "rustix" -version = "0.38.43" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.13" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "once_cell", "rustls-pki-types", @@ -1342,25 +1292,19 @@ dependencies = [ ] [[package]] -name = "rustls-pemfile" -version = "2.2.0" +name = "rustls-pki-types" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" dependencies = [ - "rustls-pki-types", + "zeroize", ] -[[package]] -name = "rustls-pki-types" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" - [[package]] name = "rustls-webpki" -version = "0.102.8" +version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "ring", "rustls-pki-types", @@ -1369,23 +1313,23 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.19" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "schannel" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1409,9 +1353,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -1419,18 +1363,28 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.217" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -1439,14 +1393,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.135" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] @@ -1478,49 +1433,40 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] [[package]] name = "slab" -version = "0.4.9" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.5.8" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "strsim" @@ -1536,9 +1482,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.96" +version = "2.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +checksum = "2f17c7e013e88258aa9543dcbe81aca68a667a9ac37cd69c9fbc07858bfe0e2f" dependencies = [ "proc-macro2", "quote", @@ -1556,9 +1502,9 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", @@ -1588,16 +1534,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.15.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ - "cfg-if", "fastrand", - "getrandom", + "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1606,7 +1551,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", ] [[package]] @@ -1620,6 +1574,17 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.9" @@ -1631,9 +1596,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.37" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "itoa", @@ -1646,15 +1611,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.19" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", @@ -1662,9 +1627,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -1672,11 +1637,10 @@ dependencies = [ [[package]] name = "tokio" -version = "1.43.0" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", "libc", "mio", @@ -1685,14 +1649,14 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", @@ -1711,20 +1675,19 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.0" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", - "rustls-pki-types", "tokio", ] [[package]] name = "tokio-util" -version = "0.7.13" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", @@ -1748,6 +1711,24 @@ dependencies = [ "tower-service", ] +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -1778,7 +1759,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" dependencies = [ "crossbeam-channel", - "thiserror", + "thiserror 1.0.69", "time", "tracing-subscriber", ] @@ -1796,9 +1777,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", "valuable", @@ -1860,9 +1841,9 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.14" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "untrusted" @@ -1872,21 +1853,16 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -1928,41 +1904,37 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasm-bindgen" -version = "0.2.100" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", + "wit-bindgen", ] [[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" +name = "wasm-bindgen" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" dependencies = [ "cfg-if", "js-sys", @@ -1973,9 +1945,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1983,31 +1955,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" dependencies = [ "js-sys", "wasm-bindgen", @@ -2015,50 +1987,96 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.52.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-targets 0.52.6", + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-registry" -version = "0.2.0" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ - "windows-result", - "windows-strings", - "windows-targets 0.52.6", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", ] [[package]] name = "windows-result" -version = "0.2.0" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-targets 0.52.6", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", ] [[package]] name = "windows-strings" -version = "0.1.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-result", - "windows-targets 0.52.6", + "windows-link 0.1.3", ] [[package]] -name = "windows-sys" -version = "0.48.0" +name = "windows-strings" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-targets 0.48.5", + "windows-link 0.2.1", ] [[package]] @@ -2080,18 +2098,21 @@ dependencies = [ ] [[package]] -name = "windows-targets" -version = "0.48.5" +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "windows-link 0.2.1", ] [[package]] @@ -2103,7 +2124,7 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", @@ -2111,10 +2132,21 @@ dependencies = [ ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" +name = "windows-targets" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] [[package]] name = "windows_aarch64_gnullvm" @@ -2123,10 +2155,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" +name = "windows_aarch64_gnullvm" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -2135,10 +2167,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] -name = "windows_i686_gnu" -version = "0.48.5" +name = "windows_aarch64_msvc" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -2146,6 +2178,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" @@ -2153,10 +2191,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] -name = "windows_i686_msvc" -version = "0.48.5" +name = "windows_i686_gnullvm" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -2165,10 +2203,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" +name = "windows_i686_msvc" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -2177,10 +2215,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" +name = "windows_x86_64_gnu" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -2189,10 +2227,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" +name = "windows_x86_64_gnullvm" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -2201,24 +2239,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "write16" -version = "1.0.0" +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "yoke" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -2226,9 +2269,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", @@ -2238,18 +2281,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", @@ -2259,15 +2302,26 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] [[package]] name = "zerovec" -version = "0.10.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -2276,9 +2330,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 49a920f..7880651 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,13 +10,13 @@ repository = "https://github.com/azerpas/bourso-api" [dependencies] bourso_api = { path = "./src/bourso_api" } -tokio = { version = "1.33.0", features = ["full"] } -anyhow = { version = "1.0.75" } +tokio = { version = "1.48.0", features = ["full"] } +anyhow = { version = "1.0.100" } clap = { version = "4.5.51", features = ["derive"] } -rpassword = { version = "7.2.0" } -directories = { version = "5.0.1" } +rpassword = { version = "7.4.0" } +directories = { version = "6.0.0" } serde = { version = "1.0.189", features = ["derive"] } -serde_json = { version = "1.0.107" } +serde_json = { version = "1.0.145" } tracing = { version = "0.1.41" } tracing-appender = { version = "0.2.3" } tracing-subscriber = { version = "0.3.20", features = ["fmt", "env-filter", "json"] } From 1f00ae01d1626ae3cfff675bfbabfd36b88c8bbb Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Fri, 7 Nov 2025 03:53:24 +0000 Subject: [PATCH 16/49] chore: bump version to 1.0.0 in Cargo.toml and Cargo.lock --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dc7fb64..f4f5c10 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,7 +124,7 @@ checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "bourso-cli" -version = "0.3.2" +version = "1.0.0" dependencies = [ "anyhow", "bourso_api", diff --git a/Cargo.toml b/Cargo.toml index 7880651..cacdeca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bourso-cli" -version = "0.3.2" +version = "1.0.0" edition = "2021" authors = ["@azerpas"] description = "BoursoBank/Boursorama CLI" From dd05c70fbee5e4c07a0cefd935385222fae29a89 Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Fri, 7 Nov 2025 04:03:12 +0000 Subject: [PATCH 17/49] chore: add workspace configuration to Cargo.toml for improved project structure --- Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index cacdeca..4f976c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,3 +24,6 @@ futures-util = { version = "0.3.31" } [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] } + +[workspace] +members = [".", "src/bourso_api"] From 3e8df0e06711329c7d2bb78292c3434eabc2090d Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Tue, 11 Nov 2025 01:56:22 +0000 Subject: [PATCH 18/49] refactor: update CLI argument type for credentials to PathBuf for better file handling --- src/cli.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 4d5c5da..12366d8 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,5 +1,7 @@ -use bourso_api::client::trade::order::OrderSide; use clap::{value_parser, Args, Parser, Subcommand}; +use std::path::PathBuf; + +use bourso_api::client::trade::order::OrderSide; // TODO: add debug option // TODO: add type to fix primitive obsession and value_parser (AccountId, QuoteInterval, QuoteLength, ...) @@ -8,8 +10,8 @@ use clap::{value_parser, Args, Parser, Subcommand}; #[command(version, author, about, long_about = None)] pub struct Cli { /// Optional path to credentials JSON file - #[arg(short, long, value_name = "FILE")] - pub credentials: Option, + #[arg(short, long, value_name = "FILE", value_parser = value_parser!(PathBuf))] + pub credentials: Option, #[command(subcommand)] pub command: Commands, From d7e05ba0f0ce65ca96c49b5b04c7bdfae2775f36 Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Tue, 11 Nov 2025 02:24:42 +0000 Subject: [PATCH 19/49] feat: implement JsonFileSettingsStore for JSON-based settings management --- src/settings/store.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/settings/store.rs b/src/settings/store.rs index 4013f2e..daf1fa7 100644 --- a/src/settings/store.rs +++ b/src/settings/store.rs @@ -71,3 +71,30 @@ impl SettingsStore for FileSettingsStore { .with_context(|| format!("Failed to persist settings file: {}", self.path().display())) } } + +pub struct JsonFileSettingsStore { + path: PathBuf, +} + +impl JsonFileSettingsStore { + pub fn new(path: PathBuf) -> Self { + Self { path } + } + + fn path(&self) -> PathBuf { + self.path.clone() + } +} + +impl SettingsStore for JsonFileSettingsStore { + fn load(&self) -> Result { + let content = fs::read_to_string(&self.path) + .with_context(|| format!("Failed to read settings file: {}", self.path.display()))?; + from_str(&content).context("Failed to deserialize settings") + } + + fn save(&self, settings: &Settings) -> Result<()> { + fs::write(self.path(), to_string_pretty(settings)?) + .with_context(|| format!("Failed to persist settings file: {}", self.path().display())) + } +} From 2f14fd0682fbee9f7185ffc1f529458b17995944 Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Tue, 11 Nov 2025 02:24:56 +0000 Subject: [PATCH 20/49] feat: integrate JsonFileSettingsStore into AppCtx for enhanced settings management --- src/lib.rs | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 50144b8..4c53f28 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,16 +8,26 @@ pub mod ux; pub use services::AuthService; pub use settings::init_logger; -pub use settings::{FileSettingsStore, Settings, SettingsStore}; +pub use settings::{FileSettingsStore, JsonFileSettingsStore, Settings, SettingsStore}; pub use ux::TextProgressBar; +pub struct AppCtx { + pub settings_store: Box, +} + pub async fn run(cli: cli::Cli) -> Result<()> { + let settings_store: Box = match cli.credentials.clone() { + Some(path) => Box::new(JsonFileSettingsStore::new(path)), + None => Box::new(FileSettingsStore::new()?), + }; + let ctx = AppCtx { settings_store }; + use cli::Commands::*; match cli.command { - Config(args) => commands::config::handle(args).await, - Accounts(args) => commands::accounts::handle(args).await, - Trade(args) => commands::trade::handle(args).await, + Config(args) => commands::config::handle(args, &ctx).await, + Accounts(args) => commands::accounts::handle(args, &ctx).await, + Trade(args) => commands::trade::handle(args, &ctx).await, Quote(args) => commands::quote::handle(args).await, - Transfer(args) => commands::transfer::handle(args).await, + Transfer(args) => commands::transfer::handle(args, &ctx).await, } } From a0c97feff9c7f5a9f289406a526c930e0cf07d56 Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Tue, 11 Nov 2025 02:25:11 +0000 Subject: [PATCH 21/49] refactor: change AuthService to use a reference for SettingsStore to improve memory management --- src/services/auth.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/services/auth.rs b/src/services/auth.rs index c183308..b159b8b 100644 --- a/src/services/auth.rs +++ b/src/services/auth.rs @@ -28,15 +28,15 @@ impl ClientFactory for DefaultClientFactory { } } -pub struct AuthService { - settings_store: Box, +pub struct AuthService<'a> { + settings_store: &'a dyn SettingsStore, credentials_provider: Box, client_factory: Box, } -impl AuthService { +impl<'a> AuthService<'a> { pub fn new( - settings_store: Box, + settings_store: &'a dyn SettingsStore, credentials_provider: Box, client_factory: Box, ) -> Self { @@ -47,9 +47,9 @@ impl AuthService { } } - pub fn with_defaults(store: Box) -> Self { + pub fn with_defaults(settings_store: &'a dyn SettingsStore) -> Self { Self::new( - store, + settings_store, Box::new(StdinCredentialsProvider), Box::new(DefaultClientFactory), ) From 33954e17f99d9c51d62543f79e4c715a491a84eb Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Tue, 11 Nov 2025 02:25:28 +0000 Subject: [PATCH 22/49] refactor: update command handlers to accept AppCtx for improved settings management --- src/commands/accounts.rs | 9 +++------ src/commands/config.rs | 7 +++---- src/commands/trade/mod.rs | 9 ++++++--- src/commands/trade/order.rs | 17 +++++++++-------- src/commands/transfer.rs | 10 +++------- src/settings/mod.rs | 2 +- 6 files changed, 25 insertions(+), 29 deletions(-) diff --git a/src/commands/accounts.rs b/src/commands/accounts.rs index 40d1d06..214f232 100644 --- a/src/commands/accounts.rs +++ b/src/commands/accounts.rs @@ -1,15 +1,12 @@ use anyhow::Result; use tracing::info; -use crate::cli::AccountsArgs; -use crate::services::AuthService; -use crate::settings::FileSettingsStore; +use crate::{cli::AccountsArgs, services::AuthService, AppCtx}; use bourso_api::account::{Account, AccountKind}; -pub async fn handle(args: AccountsArgs) -> Result<()> { - let settings_store = Box::new(FileSettingsStore::new()?); - let auth_service = AuthService::with_defaults(settings_store); +pub async fn handle(args: AccountsArgs, ctx: &AppCtx) -> Result<()> { + let auth_service = AuthService::with_defaults(&*ctx.settings_store); let Some(client) = auth_service.login().await? else { return Ok(()); diff --git a/src/commands/config.rs b/src/commands/config.rs index 930f326..5d97fa1 100644 --- a/src/commands/config.rs +++ b/src/commands/config.rs @@ -1,11 +1,10 @@ use anyhow::Result; use tracing::info; -use crate::cli::ConfigArgs; -use crate::settings::{FileSettingsStore, Settings, SettingsStore}; +use crate::{cli::ConfigArgs, settings::Settings, AppCtx}; -pub async fn handle(args: ConfigArgs) -> Result<()> { - FileSettingsStore::new()?.save(&Settings { +pub async fn handle(args: ConfigArgs, ctx: &AppCtx) -> Result<()> { + ctx.settings_store.save(&Settings { client_number: Some(args.client_number), password: None, })?; diff --git a/src/commands/trade/mod.rs b/src/commands/trade/mod.rs index 988c189..f32bbec 100644 --- a/src/commands/trade/mod.rs +++ b/src/commands/trade/mod.rs @@ -1,11 +1,14 @@ use anyhow::Result; -use crate::cli::{TradeArgs, TradeCommands}; +use crate::{ + cli::{TradeArgs, TradeCommands}, + AppCtx, +}; pub mod order; -pub async fn handle(args: TradeArgs) -> Result<()> { +pub async fn handle(args: TradeArgs, ctx: &AppCtx) -> Result<()> { match args.command { - TradeCommands::Order(o) => order::handle(o).await, + TradeCommands::Order(o) => order::handle(o, ctx).await, } } diff --git a/src/commands/trade/order.rs b/src/commands/trade/order.rs index d3b4ea7..bd9915b 100644 --- a/src/commands/trade/order.rs +++ b/src/commands/trade/order.rs @@ -1,16 +1,18 @@ use anyhow::{Context, Result}; use tracing::{info, warn}; -use crate::cli::{OrderArgs, OrderNewArgs, OrderSubcommands}; -use crate::services::AuthService; -use crate::settings::FileSettingsStore; +use crate::{ + cli::{OrderArgs, OrderNewArgs, OrderSubcommands}, + services::AuthService, + AppCtx, +}; use bourso_api::account::AccountKind; use bourso_api::client::trade::order::OrderSide; -pub async fn handle(args: OrderArgs) -> Result<()> { +pub async fn handle(args: OrderArgs, ctx: &AppCtx) -> Result<()> { match args.command { - OrderSubcommands::New(n) => new_order(n).await, + OrderSubcommands::New(n) => new_order(n, ctx).await, OrderSubcommands::List(_) => { warn!("Listing orders is coming soon."); Ok(()) @@ -22,9 +24,8 @@ pub async fn handle(args: OrderArgs) -> Result<()> { } } -async fn new_order(args: OrderNewArgs) -> Result<()> { - let store = Box::new(FileSettingsStore::new()?); - let auth = AuthService::with_defaults(store); +async fn new_order(args: OrderNewArgs, ctx: &AppCtx) -> Result<()> { + let auth = AuthService::with_defaults(&*ctx.settings_store); let Some(client) = auth.login().await? else { return Ok(()); diff --git a/src/commands/transfer.rs b/src/commands/transfer.rs index 6c7e5d1..05925eb 100644 --- a/src/commands/transfer.rs +++ b/src/commands/transfer.rs @@ -2,16 +2,12 @@ use anyhow::{Context, Result}; use futures_util::{pin_mut, StreamExt}; use tracing::info; -use crate::cli::TransferArgs; -use crate::services::AuthService; -use crate::settings::FileSettingsStore; -use crate::ux::progress::TextProgressBar; +use crate::{cli::TransferArgs, services::AuthService, ux::progress::TextProgressBar, AppCtx}; use bourso_api::client::transfer::TransferProgress; -pub async fn handle(args: TransferArgs) -> Result<()> { - let settings_store = Box::new(FileSettingsStore::new()?); - let auth_service = AuthService::with_defaults(settings_store); +pub async fn handle(args: TransferArgs, ctx: &AppCtx) -> Result<()> { + let auth_service = AuthService::with_defaults(&*ctx.settings_store); let Some(client) = auth_service.login().await? else { return Ok(()); diff --git a/src/settings/mod.rs b/src/settings/mod.rs index 080bfea..d1e7430 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -3,4 +3,4 @@ mod logging; mod store; pub use logging::init_logger; -pub use store::{FileSettingsStore, Settings, SettingsStore}; +pub use store::{FileSettingsStore, JsonFileSettingsStore, Settings, SettingsStore}; From 040cbe83f61f81c83543c7297d3eba9e482122da Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Sun, 9 Nov 2025 16:32:55 +0000 Subject: [PATCH 23/49] feat: introduce ValueError enum and ClientNumber struct for enhanced validation in bourso_api --- src/bourso_api/src/types.rs | 52 +++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/bourso_api/src/types.rs diff --git a/src/bourso_api/src/types.rs b/src/bourso_api/src/types.rs new file mode 100644 index 0000000..0578854 --- /dev/null +++ b/src/bourso_api/src/types.rs @@ -0,0 +1,52 @@ +use std::str::FromStr; + +#[derive(Debug, thiserror::Error)] +pub enum ValueError { + #[error("invalid client number: must be 8 digits (0-9)")] + ClientNumber, + #[error("invalid account id: must be 32 hexadecimal characters (0-9, a-f)")] + AccountId, + #[error("invalid symbol id: must be 6-12 alphanumeric characters (0-9, a-z, A-Z)")] + SymbolId, + #[error("invalid order quantity: must be a positive, non-zero integer")] + OrderQuantity, + #[error("invalid money amount: must be a positive, up to 2 decimal places float")] + MoneyAmount, + #[error("invalid transfer reason: must be up to 50 letters only (a-z, A-Z)")] + TransferReason, + #[error("invalid quote length: must be one of the following values: 1, 5, 30, 90, 180, 365, 1825, 3650")] + QuoteLength, + #[error("invalid quote period: must be a positive integer")] + QuotePeriod, + #[error("invalid mfa code: must be 6-12 digits (0-9)")] + MfaCode, + #[error("invalid password: must be non-empty string")] + Password, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ClientNumber(String); +impl ClientNumber { + pub fn new(s: &str) -> Result { + let t = s.trim(); + if t.len() == 8 && t.chars().all(|c| c.is_ascii_digit) { + Ok(Self(t.into())) + } else { + Err(ValueError::ClientNumber) + } + } + pub fn as_str(&self) -> &str { + &self.0 + } +} +impl FromStr for ClientNumber { + type Err = ValueError; + fn from_str(s: &str) -> Result { + Self::new(s) + } +} +impl AsRef for ClientNumber { + fn as_ref(&self) -> &str { + &self.0 + } +} From d763f43e3915a1c9eb6aff2556d001440d705367 Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Sun, 9 Nov 2025 16:39:28 +0000 Subject: [PATCH 24/49] chore: add thiserror dependency and update types.rs error messages for improved clarity --- Cargo.lock | 1 + src/bourso_api/Cargo.toml | 1 + src/bourso_api/src/lib.rs | 3 ++- src/bourso_api/src/types.rs | 8 ++++---- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f4f5c10..cf9e09f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -156,6 +156,7 @@ dependencies = [ "reqwest_cookie_store", "serde", "serde_json", + "thiserror 2.0.17", "tracing", ] diff --git a/src/bourso_api/Cargo.toml b/src/bourso_api/Cargo.toml index 7365fb7..20aedc7 100644 --- a/src/bourso_api/Cargo.toml +++ b/src/bourso_api/Cargo.toml @@ -19,6 +19,7 @@ chrono = { version = "0.4.39" } tracing = { version = "0.1.41" } futures-util = { version = "0.3.31" } async-stream = { version = "0.3.6" } +thiserror = { version = "2.0.17" } [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] } diff --git a/src/bourso_api/src/lib.rs b/src/bourso_api/src/lib.rs index 7914627..68f0513 100644 --- a/src/bourso_api/src/lib.rs +++ b/src/bourso_api/src/lib.rs @@ -1,8 +1,9 @@ pub mod account; pub mod client; pub mod constants; +pub mod types; #[cfg(not(tarpaulin_include))] pub fn get_client() -> client::BoursoWebClient { client::BoursoWebClient::new() -} \ No newline at end of file +} diff --git a/src/bourso_api/src/types.rs b/src/bourso_api/src/types.rs index 0578854..a7bbbd9 100644 --- a/src/bourso_api/src/types.rs +++ b/src/bourso_api/src/types.rs @@ -12,15 +12,15 @@ pub enum ValueError { OrderQuantity, #[error("invalid money amount: must be a positive, up to 2 decimal places float")] MoneyAmount, - #[error("invalid transfer reason: must be up to 50 letters only (a-z, A-Z)")] + #[error("invalid transfer reason: must be 0-50 letters only (a-z, A-Z)")] TransferReason, - #[error("invalid quote length: must be one of the following values: 1, 5, 30, 90, 180, 365, 1825, 3650")] + #[error("invalid quote length: must be one of: 1, 5, 30, 90, 180, 365, 1825, 3650")] QuoteLength, #[error("invalid quote period: must be a positive integer")] QuotePeriod, #[error("invalid mfa code: must be 6-12 digits (0-9)")] MfaCode, - #[error("invalid password: must be non-empty string")] + #[error("invalid password: must be a non-empty string")] Password, } @@ -29,7 +29,7 @@ pub struct ClientNumber(String); impl ClientNumber { pub fn new(s: &str) -> Result { let t = s.trim(); - if t.len() == 8 && t.chars().all(|c| c.is_ascii_digit) { + if t.len() == 8 && t.chars().all(|c| c.is_ascii_digit()) { Ok(Self(t.into())) } else { Err(ValueError::ClientNumber) From d85c3fcdc6705acd8a9bcaca2b7d3d24e85b022f Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Sun, 9 Nov 2025 16:41:34 +0000 Subject: [PATCH 25/49] refactor: update ValueError enum to use thiserror for improved error handling --- src/bourso_api/src/types.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/bourso_api/src/types.rs b/src/bourso_api/src/types.rs index a7bbbd9..2b3d43b 100644 --- a/src/bourso_api/src/types.rs +++ b/src/bourso_api/src/types.rs @@ -1,6 +1,7 @@ use std::str::FromStr; +use thiserror::Error; -#[derive(Debug, thiserror::Error)] +#[derive(Debug, Error)] pub enum ValueError { #[error("invalid client number: must be 8 digits (0-9)")] ClientNumber, From b859a9a1438085d07b9fbbaa54908462b515a34c Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Sun, 9 Nov 2025 16:43:18 +0000 Subject: [PATCH 26/49] feat: add AccountId struct for better validation --- src/bourso_api/src/types.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/bourso_api/src/types.rs b/src/bourso_api/src/types.rs index 2b3d43b..0c4a436 100644 --- a/src/bourso_api/src/types.rs +++ b/src/bourso_api/src/types.rs @@ -51,3 +51,30 @@ impl AsRef for ClientNumber { &self.0 } } + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct AccountId(String); +impl AccountId { + pub fn new(s: &str) -> Result { + let t = s.trim(); + if t.len() == 32 && t.chars().all(|c| c.is_ascii_hexdigit()) { + Ok(Self(t.into())) + } else { + Err(ValueError::AccountId) + } + } + pub fn as_str(&self) -> &str { + &self.0 + } +} +impl FromStr for AccountId { + type Err = ValueError; + fn from_str(s: &str) -> Result { + Self::new(s) + } +} +impl AsRef for AccountId { + fn as_ref(&self) -> &str { + &self.0 + } +} From 46c067298c49d2c879eb99897a058e66922a78ef Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Sun, 9 Nov 2025 16:46:01 +0000 Subject: [PATCH 27/49] feat: introduce SymbolId struct for better validation --- src/bourso_api/src/types.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/bourso_api/src/types.rs b/src/bourso_api/src/types.rs index 0c4a436..1d923e3 100644 --- a/src/bourso_api/src/types.rs +++ b/src/bourso_api/src/types.rs @@ -78,3 +78,30 @@ impl AsRef for AccountId { &self.0 } } + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct SymbolId(String); +impl SymbolId { + pub fn new(s: &str) -> Result { + let t = s.trim(); + if (6..=12).contains(&t.len()) && t.chars().all(|c| c.is_ascii_alphanumeric()) { + Ok(Self(t.into())) + } else { + Err(ValueError::SymbolId) + } + } + pub fn as_str(&self) -> &str { + &self.0 + } +} +impl FromStr for SymbolId { + type Err = ValueError; + fn from_str(s: &str) -> Result { + Self::new(s) + } +} +impl AsRef for SymbolId { + fn as_ref(&self) -> &str { + &self.0 + } +} From ae1060e6fc8a2c6e1574de1e62ff3eee12654d98 Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Sun, 9 Nov 2025 16:47:12 +0000 Subject: [PATCH 28/49] feat: add OrderQuantity struct for better validation --- src/bourso_api/src/types.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/bourso_api/src/types.rs b/src/bourso_api/src/types.rs index 1d923e3..13508f8 100644 --- a/src/bourso_api/src/types.rs +++ b/src/bourso_api/src/types.rs @@ -105,3 +105,25 @@ impl AsRef for SymbolId { &self.0 } } + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct OrderQuantity(u64); +impl OrderQuantity { + pub fn new(v: u64) -> Result { + if v >= 1 { + Ok(Self(v)) + } else { + Err(ValueError::OrderQuantity) + } + } + pub fn get(self) -> u64 { + self.0 + } +} +impl FromStr for OrderQuantity { + type Err = ValueError; + fn from_str(s: &str) -> Result { + let v: u64 = s.parse().map_err(|_| ValueError::OrderQuantity)?; + Self::new(v) + } +} From c0006977a469fc585cb7fa3b796426657570a2c6 Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Sun, 9 Nov 2025 16:54:56 +0000 Subject: [PATCH 29/49] feat: add MoneyAmount struct for better validation --- src/bourso_api/src/types.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/bourso_api/src/types.rs b/src/bourso_api/src/types.rs index 13508f8..6aca992 100644 --- a/src/bourso_api/src/types.rs +++ b/src/bourso_api/src/types.rs @@ -127,3 +127,25 @@ impl FromStr for OrderQuantity { Self::new(v) } } + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct MoneyAmount(f64); +impl MoneyAmount { + pub fn new(v: f64) -> Result { + if v > 0.0 && v.fract().abs() <= 0.02 { + Ok(Self(v)) + } else { + Err(ValueError::MoneyAmount) + } + } + pub fn get(self) -> f64 { + self.0 + } +} +impl FromStr for MoneyAmount { + type Err = ValueError; + fn from_str(s: &str) -> Result { + let v: f64 = s.parse().map_err(|_| ValueError::MoneyAmount)?; + Self::new(v) + } +} From 2b587280f55fa09a9ad4047fb9cede34cc1b63c3 Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Sun, 9 Nov 2025 16:56:37 +0000 Subject: [PATCH 30/49] feat: add TransferReason struct for better validation --- src/bourso_api/src/types.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/bourso_api/src/types.rs b/src/bourso_api/src/types.rs index 6aca992..a776a9f 100644 --- a/src/bourso_api/src/types.rs +++ b/src/bourso_api/src/types.rs @@ -149,3 +149,32 @@ impl FromStr for MoneyAmount { Self::new(v) } } + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct TransferReason(String); +impl TransferReason { + pub fn new(s: &str) -> Result { + let t = s.trim(); + if t.len() > 50 { + return Err(ValueError::TransferReason); + } + if !t.chars().all(|c| c.is_ascii_alphabetic()) { + return Err(ValueError::TransferReason); + } + Ok(Self(t.into())) + } + pub fn as_str(&self) -> &str { + &self.0 + } +} +impl FromStr for TransferReason { + type Err = ValueError; + fn from_str(s: &str) -> Result { + Self::new(s) + } +} +impl AsRef for TransferReason { + fn as_ref(&self) -> &str { + &self.0 + } +} From 58f33b066c3f22a8930dcc8786d90156dd885988 Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Tue, 11 Nov 2025 01:29:28 +0000 Subject: [PATCH 31/49] feat: add QuoteLength enum for better validation --- src/bourso_api/src/types.rs | 42 +++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/bourso_api/src/types.rs b/src/bourso_api/src/types.rs index a776a9f..4d8b978 100644 --- a/src/bourso_api/src/types.rs +++ b/src/bourso_api/src/types.rs @@ -178,3 +178,45 @@ impl AsRef for TransferReason { &self.0 } } + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum QuoteLength { + D1, + D5, + D30, + D90, + D180, + D365, + D1825, + D3650, +} +impl QuoteLength { + pub fn days(self) -> i64 { + match self { + QuoteLength::D1 => 1, + QuoteLength::D5 => 5, + QuoteLength::D30 => 30, + QuoteLength::D90 => 90, + QuoteLength::D180 => 180, + QuoteLength::D365 => 365, + QuoteLength::D1825 => 1825, + QuoteLength::D3650 => 3650, + } + } +} +impl FromStr for QuoteLength { + type Err = ValueError; + fn from_str(s: &str) -> Result { + match s.trim().parse::() { + Ok(1) => Ok(QuoteLength::D1), + Ok(5) => Ok(QuoteLength::D5), + Ok(30) => Ok(QuoteLength::D30), + Ok(90) => Ok(QuoteLength::D90), + Ok(180) => Ok(QuoteLength::D180), + Ok(365) => Ok(QuoteLength::D365), + Ok(1825) => Ok(QuoteLength::D1825), + Ok(3650) => Ok(QuoteLength::D3650), + _ => Err(ValueError::QuoteLength), + } + } +} From 1f8743ecd5919de98fa3eae3d9d4d34e7b98d53b Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Tue, 11 Nov 2025 01:31:33 +0000 Subject: [PATCH 32/49] feat: add QuotePeriod struct for better validation --- src/bourso_api/src/types.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/bourso_api/src/types.rs b/src/bourso_api/src/types.rs index 4d8b978..ba63907 100644 --- a/src/bourso_api/src/types.rs +++ b/src/bourso_api/src/types.rs @@ -220,3 +220,26 @@ impl FromStr for QuoteLength { } } } + +// TODO: add support for other periods +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct QuotePeriod(i64); +impl QuotePeriod { + pub fn new(v: i64) -> Result { + if v != 0 { + Ok(Self(v)) + } else { + Err(ValueError::QuotePeriod) + } + } + pub fn value(self) -> i64 { + self.0 + } +} +impl FromStr for QuotePeriod { + type Err = ValueError; + fn from_str(s: &str) -> Result { + let v: i64 = s.trim().parse().map_err(|_| ValueError::QuotePeriod)?; + Self::new(v) + } +} From 594ed69e6e0625dfd7463ea930030394eacb5469 Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Tue, 11 Nov 2025 01:32:17 +0000 Subject: [PATCH 33/49] feat: add MfaCode struct for better validation --- src/bourso_api/src/types.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/bourso_api/src/types.rs b/src/bourso_api/src/types.rs index ba63907..5451eef 100644 --- a/src/bourso_api/src/types.rs +++ b/src/bourso_api/src/types.rs @@ -243,3 +243,30 @@ impl FromStr for QuotePeriod { Self::new(v) } } + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct MfaCode(String); +impl MfaCode { + pub fn new(s: &str) -> Result { + let t = s.trim(); + if (6..=12).contains(&t.len()) && t.chars().all(|c| c.is_ascii_digit()) { + Ok(Self(t.into())) + } else { + Err(ValueError::MfaCode) + } + } + pub fn as_str(&self) -> &str { + &self.0 + } +} +impl FromStr for MfaCode { + type Err = ValueError; + fn from_str(s: &str) -> Result { + Self::new(s) + } +} +impl AsRef for MfaCode { + fn as_ref(&self) -> &str { + &self.0 + } +} From 3f1e53899eef5a88ea60cf9c272887d3cfe7035e Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Tue, 11 Nov 2025 01:32:32 +0000 Subject: [PATCH 34/49] feat: add Password struct for better validation --- src/bourso_api/src/types.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/bourso_api/src/types.rs b/src/bourso_api/src/types.rs index 5451eef..c0f2d84 100644 --- a/src/bourso_api/src/types.rs +++ b/src/bourso_api/src/types.rs @@ -270,3 +270,30 @@ impl AsRef for MfaCode { &self.0 } } + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Password(String); +impl Password { + pub fn new(s: &str) -> Result { + let t = s.trim(); + if !t.is_empty() { + Ok(Self(t.into())) + } else { + Err(ValueError::Password) + } + } + pub fn as_str(&self) -> &str { + &self.0 + } +} +impl FromStr for Password { + type Err = ValueError; + fn from_str(s: &str) -> Result { + Self::new(s) + } +} +impl AsRef for Password { + fn as_ref(&self) -> &str { + &self.0 + } +} From 88dc88d6c4bab6242c703c631823a69f78ffbdfa Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Tue, 11 Nov 2025 03:49:30 +0000 Subject: [PATCH 35/49] feat: enhance ClientNumber and Password structs with serialization support --- src/bourso_api/src/types.rs | 29 +++++++++++++++++++++++++++-- src/settings/store.rs | 5 +++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/bourso_api/src/types.rs b/src/bourso_api/src/types.rs index c0f2d84..054338b 100644 --- a/src/bourso_api/src/types.rs +++ b/src/bourso_api/src/types.rs @@ -1,3 +1,4 @@ +use serde::{Deserialize, Serialize}; use std::str::FromStr; use thiserror::Error; @@ -25,7 +26,8 @@ pub enum ValueError { Password, } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(try_from = "String", into = "String")] pub struct ClientNumber(String); impl ClientNumber { pub fn new(s: &str) -> Result { @@ -51,6 +53,17 @@ impl AsRef for ClientNumber { &self.0 } } +impl TryFrom for ClientNumber { + type Error = ValueError; + fn try_from(value: String) -> Result { + Self::new(&value) + } +} +impl From for String { + fn from(value: ClientNumber) -> Self { + value.0 + } +} #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct AccountId(String); @@ -271,7 +284,8 @@ impl AsRef for MfaCode { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(try_from = "String", into = "String")] pub struct Password(String); impl Password { pub fn new(s: &str) -> Result { @@ -297,3 +311,14 @@ impl AsRef for Password { &self.0 } } +impl TryFrom for Password { + type Error = ValueError; + fn try_from(value: String) -> Result { + Self::new(&value) + } +} +impl From for String { + fn from(value: Password) -> Self { + value.0 + } +} diff --git a/src/settings/store.rs b/src/settings/store.rs index daf1fa7..120741c 100644 --- a/src/settings/store.rs +++ b/src/settings/store.rs @@ -1,4 +1,5 @@ use anyhow::{anyhow, Context, Result}; +use bourso_api::types::{ClientNumber, Password}; use directories::ProjectDirs; use serde::{Deserialize, Serialize}; use serde_json::{from_str, to_string_pretty}; @@ -9,9 +10,9 @@ use crate::settings::consts::{APP_NAME, APP_ORGANIZATION, APP_QUALIFIER, SETTING #[derive(Serialize, Deserialize, Default)] pub struct Settings { #[serde(rename = "clientNumber")] - pub client_number: Option, + pub client_number: Option, #[serde(rename = "password")] - pub password: Option, + pub password: Option, } pub trait SettingsStore { From 73f3a5cba24be71a96dcf37401432072b8973efd Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Tue, 11 Nov 2025 03:49:45 +0000 Subject: [PATCH 36/49] feat: update CLI argument types to use new validation structs for improved type safety --- src/cli.rs | 65 ++++++++++++++++++++++++++---------------------------- 1 file changed, 31 insertions(+), 34 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 12366d8..eb08478 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,7 +1,13 @@ use clap::{value_parser, Args, Parser, Subcommand}; use std::path::PathBuf; -use bourso_api::client::trade::order::OrderSide; +use bourso_api::{ + client::trade::order::OrderSide, + types::{ + AccountId, ClientNumber, MoneyAmount, OrderQuantity, QuoteLength, QuotePeriod, SymbolId, + TransferReason, + }, +}; // TODO: add debug option // TODO: add type to fix primitive obsession and value_parser (AccountId, QuoteInterval, QuoteLength, ...) @@ -38,8 +44,8 @@ pub enum Commands { #[derive(Args)] pub struct ConfigArgs { /// Your client number - #[arg(short, long, value_name = "ID")] - pub client_number: String, + #[arg(short, long, value_name = "ID", value_parser = value_parser!(ClientNumber))] + pub client_number: ClientNumber, } #[derive(Args)] @@ -97,20 +103,20 @@ pub struct OrderListArgs {} #[derive(Args)] pub struct OrderNewArgs { /// Account to use by its ID (32 hex chars), you can get it with the `bourso accounts` command - #[arg(short, long, value_name = "ID", value_parser = parse_account_id)] - pub account: String, + #[arg(short, long, value_name = "ID", value_parser = value_parser!(AccountId))] + pub account: AccountId, /// Side of the order (buy/sell) - #[arg(long, value_parser = clap::value_parser!(OrderSide))] + #[arg(long, value_parser = value_parser!(OrderSide))] pub side: OrderSide, /// Symbol ID of the order (e.g: "1rTCW8") - #[arg(long, value_name = "ID")] - pub symbol: String, + #[arg(long, value_name = "ID", value_parser = value_parser!(SymbolId))] + pub symbol: SymbolId, /// Quantity of the order (e.g: 1) - #[arg(short, long, value_parser = value_parser!(u64).range(1..))] - pub quantity: u64, + #[arg(short, long, value_parser = value_parser!(OrderQuantity))] + pub quantity: OrderQuantity, } #[derive(Args)] @@ -119,20 +125,20 @@ pub struct OrderCancelArgs {} #[derive(Args)] pub struct QuoteArgs { /// Symbol ID of the stock (e.g: "1rTCW8") - #[arg(long, value_name = "ID")] - pub symbol: String, + #[arg(long, value_name = "ID", value_parser = value_parser!(SymbolId))] + pub symbol: SymbolId, /// Length period of the stock (1, 5, 30, 90, 180, 365, 1825, 3650) #[arg( long, default_value = "30", - value_parser = value_parser!(i64).range(1..=3650) + value_parser = value_parser!(QuoteLength) )] - pub length: i64, + pub length: QuoteLength, - /// Interval of the stock (use "0" for default) - #[arg(long, default_value = "0", value_parser = value_parser!(i64).range(0..))] - pub interval: i64, + /// Period of the stock (use "0" for default) + #[arg(long, default_value = "0", value_parser = value_parser!(QuotePeriod))] + pub period: QuotePeriod, #[command(subcommand)] pub view: Option, @@ -159,27 +165,18 @@ pub enum QuoteView { #[derive(Args)] pub struct TransferArgs { /// Source account ID (32 hex chars), you can get it with the `bourso accounts` command - #[arg(long = "from", value_name = "ID", value_parser = parse_account_id)] - pub from_account: String, + #[arg(long = "from", value_name = "ID", value_parser = value_parser!(AccountId))] + pub from_account: AccountId, /// Destination account ID (32 hex chars), you can get it with the `bourso accounts` command - #[arg(long = "to", value_name = "ID", value_parser = parse_account_id)] - pub to_account: String, + #[arg(long = "to", value_name = "ID", value_parser = value_parser!(AccountId))] + pub to_account: AccountId, /// Amount to transfer - #[arg(long)] - pub amount: String, + #[arg(long, value_parser = value_parser!(MoneyAmount))] + pub amount: MoneyAmount, /// Reason for the transfer (max 50 chars) - #[arg(long)] - pub reason: Option, -} - -fn parse_account_id(s: &str) -> Result { - let t = s.trim(); - if t.len() == 32 && t.chars().all(|c| c.is_ascii_hexdigit()) { - Ok(t.to_owned()) - } else { - Err("Account ID must be 32 hex characters (0-9, a-f)".into()) - } + #[arg(long, value_parser = value_parser!(TransferReason))] + pub reason: Option, } From ce61000fd14c5e07ce5feb33a154fffae089618e Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Tue, 11 Nov 2025 04:06:32 +0000 Subject: [PATCH 37/49] feat: implement TryFrom and From traits for MfaCode to enhance type conversion --- src/bourso_api/src/types.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/bourso_api/src/types.rs b/src/bourso_api/src/types.rs index 054338b..715cdf1 100644 --- a/src/bourso_api/src/types.rs +++ b/src/bourso_api/src/types.rs @@ -283,6 +283,17 @@ impl AsRef for MfaCode { &self.0 } } +impl TryFrom for MfaCode { + type Error = ValueError; + fn try_from(value: String) -> Result { + Self::new(&value) + } +} +impl From for String { + fn from(value: MfaCode) -> Self { + value.0 + } +} #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(try_from = "String", into = "String")] From 6a26dd721778907c5bc727544f6992e0ab3e1256 Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Tue, 11 Nov 2025 04:25:48 +0000 Subject: [PATCH 38/49] feat: refactor argument handling in quote, transfer, and order commands for improved type safety and clarity --- src/commands/quote.rs | 6 +++++- src/commands/trade/order.rs | 8 +++++--- src/commands/transfer.rs | 22 +++++++++++++--------- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/commands/quote.rs b/src/commands/quote.rs index 07cdc66..5ee0f3a 100644 --- a/src/commands/quote.rs +++ b/src/commands/quote.rs @@ -8,7 +8,11 @@ pub async fn handle(args: QuoteArgs) -> Result<()> { let client = bourso_api::get_client(); let quotes = client - .get_ticks(&args.symbol, args.length, args.interval) + .get_ticks( + args.symbol.as_str(), + args.length.days(), + args.period.value(), + ) .await?; match args.view { diff --git a/src/commands/trade/order.rs b/src/commands/trade/order.rs index bd9915b..a878e80 100644 --- a/src/commands/trade/order.rs +++ b/src/commands/trade/order.rs @@ -35,14 +35,16 @@ async fn new_order(args: OrderNewArgs, ctx: &AppCtx) -> Result<()> { let accounts = client.get_accounts(Some(AccountKind::Trading)).await?; let account = accounts .iter() - .find(|a| a.id == args.account) + .find(|a| a.id == args.account.as_str()) // TODO: compare AccountId instead of String .context("Account not found. Are you sure you have access to it? Run `bourso accounts` to list your accounts")?; let side: OrderSide = args.side; - let quantity: usize = args.quantity as usize; + let quantity: usize = args.quantity.get() as usize; let symbol = args.symbol; - let _ = client.order(side, account, &symbol, quantity, None).await?; + let _ = client + .order(side, account, symbol.as_str(), quantity, None) + .await?; info!("Order submitted ✅"); Ok(()) diff --git a/src/commands/transfer.rs b/src/commands/transfer.rs index 05925eb..73d729f 100644 --- a/src/commands/transfer.rs +++ b/src/commands/transfer.rs @@ -13,22 +13,24 @@ pub async fn handle(args: TransferArgs, ctx: &AppCtx) -> Result<()> { return Ok(()); }; - let from_account_id = args.from_account; - let to_account_id = args.to_account; - let amount: f64 = args.amount.parse()?; - let reason = args.reason; - let accounts = client.get_accounts(None).await?; + let from_account = accounts .iter() - .find(|a| a.id == from_account_id) + .find(|a| a.id == args.from_account.as_str()) // TODO: compare AccountId instead of String .context("From account not found. Are you sure you have access to it? Run `bourso accounts` to list your accounts")?; + let to_account = accounts .iter() - .find(|a| a.id == to_account_id) + .find(|a| a.id == args.to_account.as_str()) // TODO: compare AccountId instead of String .context("To account not found. Are you sure you have access to it? Run `bourso accounts` to list your accounts")?; - let stream = client.transfer_funds(amount, from_account.clone(), to_account.clone(), reason); + let stream = client.transfer_funds( + args.amount.get(), + from_account.clone(), + to_account.clone(), + args.reason.map(|r| r.as_str().to_string()), + ); let bar = TextProgressBar::new(30usize); pin_mut!(stream); @@ -43,7 +45,9 @@ pub async fn handle(args: TransferArgs, ctx: &AppCtx) -> Result<()> { info!( "Transfer of {} from account {} to account {} successful ✅", - amount, from_account.id, to_account.id + args.amount.get(), + from_account.id, + to_account.id ); Ok(()) From 37288b802cfae5b17a16e91d4fc79ef699f244fa Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Tue, 11 Nov 2025 04:25:54 +0000 Subject: [PATCH 39/49] fix: correct logic in QuotePeriod creation to ensure valid initialization --- src/bourso_api/src/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bourso_api/src/types.rs b/src/bourso_api/src/types.rs index 715cdf1..1237fb2 100644 --- a/src/bourso_api/src/types.rs +++ b/src/bourso_api/src/types.rs @@ -239,7 +239,7 @@ impl FromStr for QuoteLength { pub struct QuotePeriod(i64); impl QuotePeriod { pub fn new(v: i64) -> Result { - if v != 0 { + if v == 0 { Ok(Self(v)) } else { Err(ValueError::QuotePeriod) From 27ca7e0b9f7499458c8bb505973c8c5f2c9d3b29 Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Tue, 11 Nov 2025 04:26:02 +0000 Subject: [PATCH 40/49] feat: enhance authentication flow by introducing MFA code handling and updating password type to improve type safety --- src/services/auth.rs | 57 +++++++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/src/services/auth.rs b/src/services/auth.rs index b159b8b..99e4fce 100644 --- a/src/services/auth.rs +++ b/src/services/auth.rs @@ -1,20 +1,28 @@ -use anyhow::{Context, Result}; +use anyhow::Result; use tracing::{info, warn}; use crate::settings::SettingsStore; -use bourso_api::client::{error::ClientError, BoursoWebClient}; +use bourso_api::{ + client::{error::ClientError, BoursoWebClient}, + types::{ClientNumber, MfaCode, Password}, +}; // TODO: fix naming, too many mismatches with customer_id / username / client_id / client_number // TODO: does it make sense to have MFA handling in the CLI? pub trait CredentialsProvider: Send + Sync { - fn read_password(&self, prompt: &str) -> Result; + fn read_password(&self, prompt: &str) -> Result; + fn read_mfa_code(&self, prompt: &str) -> Result; } pub struct StdinCredentialsProvider; impl CredentialsProvider for StdinCredentialsProvider { - fn read_password(&self, prompt: &str) -> Result { + fn read_password(&self, prompt: &str) -> Result { println!("{prompt}"); - Ok(rpassword::read_password()?) + Ok(rpassword::read_password()?.try_into()?) + } + fn read_mfa_code(&self, prompt: &str) -> Result { + println!("{prompt}"); + Ok(rpassword::read_password()?.try_into()?) } } @@ -57,30 +65,33 @@ impl<'a> AuthService<'a> { pub async fn login(&self) -> Result> { let settings = self.settings_store.load()?; - let Some(client_number) = settings.client_number else { + let Some(client_number) = settings.client_number.clone() else { warn!("No client number found in settings, please run `bourso config` to set it"); return Ok(None); }; - info!("We'll try to log you in with your customer id: {client_number}"); + info!( + "We'll try to log you in with your customer id: {:?}", + client_number.as_str() + ); info!("If you want to change it, you can run `bourso config` to set it"); println!(); - let password = match settings.password { - Some(password) => password, + let password = match settings.password.as_ref() { + Some(password) => password.clone(), None => { info!("We'll need your password to log you in. It will not be stored."); self.credentials_provider - .read_password("Enter your password (hidden):") - .context("Failed to read password")? - .trim() - .to_string() + .read_password("Enter your password (hidden):")? } }; let mut client = self.client_factory.new_client(); client.init_session().await?; - match client.login(&client_number, &password).await { + match client + .login(client_number.as_str(), password.as_str()) + .await + { Ok(_) => { info!("Login successful ✅"); Ok(Some(client)) @@ -98,15 +109,17 @@ impl<'a> AuthService<'a> { async fn handle_mfa( &self, mut client: BoursoWebClient, - client_number: &str, - password: &str, + client_number: &ClientNumber, + password: &Password, ) -> Result> { let mut mfa_count = 0usize; loop { if mfa_count == 2 { warn!("MFA threshold reached. Reinitializing session and logging in again."); client.init_session().await?; - client.login(client_number, password).await?; + client + .login(client_number.as_str(), password.as_str()) + .await?; info!("Login successful ✅"); return Ok(Some(client)); } @@ -114,12 +127,12 @@ impl<'a> AuthService<'a> { let (otp_id, token_form, mfa_type) = client.request_mfa().await?; let code = self .credentials_provider - .read_password("Enter your MFA code (hidden):") - .context("Failed to read MFA code")? - .trim() - .to_string(); + .read_mfa_code("Enter your MFA code (hidden):")?; - match client.submit_mfa(mfa_type, otp_id, code, token_form).await { + match client + .submit_mfa(mfa_type, otp_id, code.as_str().to_string(), token_form) + .await + { Ok(_) => { info!("MFA successfully submitted ✅"); return Ok(Some(client)); From 73dde0d387739371f9710afe52848bd2fefcee1b Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Tue, 11 Nov 2025 04:27:55 +0000 Subject: [PATCH 41/49] fix: update error message for QuotePeriod to specify valid value as 0 --- src/bourso_api/src/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bourso_api/src/types.rs b/src/bourso_api/src/types.rs index 1237fb2..5aa561f 100644 --- a/src/bourso_api/src/types.rs +++ b/src/bourso_api/src/types.rs @@ -18,7 +18,7 @@ pub enum ValueError { TransferReason, #[error("invalid quote length: must be one of: 1, 5, 30, 90, 180, 365, 1825, 3650")] QuoteLength, - #[error("invalid quote period: must be a positive integer")] + #[error("invalid quote period: must be 0")] QuotePeriod, #[error("invalid mfa code: must be 6-12 digits (0-9)")] MfaCode, From ade0e7cfc6f902ff5f8fe3213de3f71e5bc7d9a4 Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Thu, 13 Nov 2025 03:26:55 +0000 Subject: [PATCH 42/49] refactor: replace FileSettingsStore and JsonFileSettingsStore with FsSettingsStore for improved settings management --- src/lib.rs | 17 +++++--- src/settings/mod.rs | 2 +- src/settings/store.rs | 98 ++++++++++++++++++------------------------- 3 files changed, 52 insertions(+), 65 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4c53f28..6185724 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,7 @@ pub mod ux; pub use services::AuthService; pub use settings::init_logger; -pub use settings::{FileSettingsStore, JsonFileSettingsStore, Settings, SettingsStore}; +pub use settings::{FsSettingsStore, Settings, SettingsStore}; pub use ux::TextProgressBar; pub struct AppCtx { @@ -16,14 +16,19 @@ pub struct AppCtx { } pub async fn run(cli: cli::Cli) -> Result<()> { - let settings_store: Box = match cli.credentials.clone() { - Some(path) => Box::new(JsonFileSettingsStore::new(path)), - None => Box::new(FileSettingsStore::new()?), + let cli::Cli { + credentials, + command, + } = cli; + + let settings_store: Box = match credentials { + Some(path) => Box::new(FsSettingsStore::from_path(path)), + None => Box::new(FsSettingsStore::from_default_config_dir()?), }; let ctx = AppCtx { settings_store }; - use cli::Commands::*; - match cli.command { + use cli::Commands::*; // TODO: do I need it ? + match command { Config(args) => commands::config::handle(args, &ctx).await, Accounts(args) => commands::accounts::handle(args, &ctx).await, Trade(args) => commands::trade::handle(args, &ctx).await, diff --git a/src/settings/mod.rs b/src/settings/mod.rs index d1e7430..0b6f10e 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -3,4 +3,4 @@ mod logging; mod store; pub use logging::init_logger; -pub use store::{FileSettingsStore, JsonFileSettingsStore, Settings, SettingsStore}; +pub use store::{FsSettingsStore, Settings, SettingsStore}; diff --git a/src/settings/store.rs b/src/settings/store.rs index 120741c..391ffc3 100644 --- a/src/settings/store.rs +++ b/src/settings/store.rs @@ -3,7 +3,11 @@ use bourso_api::types::{ClientNumber, Password}; use directories::ProjectDirs; use serde::{Deserialize, Serialize}; use serde_json::{from_str, to_string_pretty}; -use std::{fs, path::PathBuf}; +use std::{ + fs::{create_dir_all, read_to_string, write}, + io::ErrorKind, + path::PathBuf, +}; use crate::settings::consts::{APP_NAME, APP_ORGANIZATION, APP_QUALIFIER, SETTINGS_FILE}; @@ -20,82 +24,60 @@ pub trait SettingsStore { fn save(&self, settings: &Settings) -> Result<()>; } -pub struct FileSettingsStore { - directory: PathBuf, // platform config directory (from ProjectDirs) - file: &'static str, // "settings.json" +pub struct FsSettingsStore { + path: PathBuf, + create_if_missing: bool, } -impl FileSettingsStore { - pub fn new() -> Result { +impl FsSettingsStore { + /// Default location (XDG / platform config dir + SETTINGS_FILE) + pub fn from_default_config_dir() -> Result { let project_dirs = ProjectDirs::from(APP_QUALIFIER, APP_ORGANIZATION, APP_NAME) .ok_or_else(|| anyhow!("Could not determine project directories"))?; Ok(Self { - directory: project_dirs.config_dir().to_path_buf(), - file: SETTINGS_FILE, + path: project_dirs.config_dir().join(SETTINGS_FILE), + create_if_missing: true, }) } - fn path(&self) -> PathBuf { - self.directory.join(self.file) - } -} - -impl SettingsStore for FileSettingsStore { - fn load(&self) -> Result { - fs::create_dir_all(&self.directory).with_context(|| { - format!( - "Failed to create settings directory: {}", - self.directory.display() - ) - })?; - let path = self.path(); - let content = match fs::read_to_string(&path) { - Ok(content) => content, - Err(_) => { - let defaults = Settings::default(); - self.save(&defaults)?; - return Ok(defaults); - } - }; - from_str(&content).context("Failed to deserialize settings") + /// Arbitrary path (e.g. provided via CLI) + pub fn from_path(path: PathBuf) -> Self { + Self { + path, + create_if_missing: false, + } } - fn save(&self, settings: &Settings) -> Result<()> { - fs::create_dir_all(&self.directory).with_context(|| { - format!( - "Failed to create settings directory: {}", - self.directory.display() - ) - })?; - fs::write(self.path(), to_string_pretty(settings)?) - .with_context(|| format!("Failed to persist settings file: {}", self.path().display())) + fn ensure_directory(&self) -> Result<()> { + if let Some(directory) = self.path.parent() { + create_dir_all(directory).context("Failed to create settings directory")?; + } + Ok(()) } } -pub struct JsonFileSettingsStore { - path: PathBuf, -} +impl SettingsStore for FsSettingsStore { + fn load(&self) -> Result { + self.ensure_directory()?; -impl JsonFileSettingsStore { - pub fn new(path: PathBuf) -> Self { - Self { path } - } + match read_to_string(&self.path) { + Ok(content) => from_str(&content).context("Failed to deserialize settings"), - fn path(&self) -> PathBuf { - self.path.clone() - } -} + Err(e) if self.create_if_missing && e.kind() == ErrorKind::NotFound => { + // Only for "default config" mode AND only if the file is missing + let defaults = Settings::default(); + self.save(&defaults)?; + Ok(defaults) + } -impl SettingsStore for JsonFileSettingsStore { - fn load(&self) -> Result { - let content = fs::read_to_string(&self.path) - .with_context(|| format!("Failed to read settings file: {}", self.path.display()))?; - from_str(&content).context("Failed to deserialize settings") + Err(e) => Err(e).context("Failed to read settings file"), + } } fn save(&self, settings: &Settings) -> Result<()> { - fs::write(self.path(), to_string_pretty(settings)?) - .with_context(|| format!("Failed to persist settings file: {}", self.path().display())) + self.ensure_directory()?; + + write(&self.path, to_string_pretty(settings)?).context("Failed to persist settings file") } } From 5effad7e258119c36ec1e1fe3e0abbfef8d23ffe Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Thu, 13 Nov 2025 03:31:05 +0000 Subject: [PATCH 43/49] refactor: streamline command handling by qualifying command variants and consolidate settings imports --- src/lib.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6185724..e553c0a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,8 +7,7 @@ pub mod settings; pub mod ux; pub use services::AuthService; -pub use settings::init_logger; -pub use settings::{FsSettingsStore, Settings, SettingsStore}; +pub use settings::{init_logger, FsSettingsStore, Settings, SettingsStore}; pub use ux::TextProgressBar; pub struct AppCtx { @@ -27,12 +26,11 @@ pub async fn run(cli: cli::Cli) -> Result<()> { }; let ctx = AppCtx { settings_store }; - use cli::Commands::*; // TODO: do I need it ? match command { - Config(args) => commands::config::handle(args, &ctx).await, - Accounts(args) => commands::accounts::handle(args, &ctx).await, - Trade(args) => commands::trade::handle(args, &ctx).await, - Quote(args) => commands::quote::handle(args).await, - Transfer(args) => commands::transfer::handle(args, &ctx).await, + cli::Commands::Config(args) => commands::config::handle(args, &ctx).await, + cli::Commands::Accounts(args) => commands::accounts::handle(args, &ctx).await, + cli::Commands::Trade(args) => commands::trade::handle(args, &ctx).await, + cli::Commands::Quote(args) => commands::quote::handle(args).await, + cli::Commands::Transfer(args) => commands::transfer::handle(args, &ctx).await, } } From e1c4c38faa4e05f0a714b282f9bfba6193032c9b Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Thu, 13 Nov 2025 03:42:10 +0000 Subject: [PATCH 44/49] refactor: replace fs::create_dir_all with a direct import for cleaner code --- src/settings/logging.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/settings/logging.rs b/src/settings/logging.rs index 2cb89f1..c64854e 100644 --- a/src/settings/logging.rs +++ b/src/settings/logging.rs @@ -1,7 +1,7 @@ use anyhow::{anyhow, Result}; use directories::ProjectDirs; use std::{ - fs, + fs::create_dir_all, io::{stderr, IsTerminal}, }; use tracing_appender::rolling; @@ -21,7 +21,7 @@ pub fn init_logger() -> Result<()> { .ok_or_else(|| anyhow!("Could not determine project directories"))?; let directory = project_dirs.data_dir(); - fs::create_dir_all(directory)?; + create_dir_all(directory)?; let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(DEFAULT_LOG_LEVEL)); From b0c51475327650d290cc7ef8a1d7de4c56ce3abc Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Thu, 20 Nov 2025 01:16:56 +0000 Subject: [PATCH 45/49] refactor: update AuthService initialization to use references for settings store and streamline password/MFA handling --- src/commands/accounts.rs | 2 +- src/commands/trade/order.rs | 2 +- src/commands/transfer.rs | 2 +- src/services/auth.rs | 38 +++++++++++++++++-------------------- 4 files changed, 20 insertions(+), 24 deletions(-) diff --git a/src/commands/accounts.rs b/src/commands/accounts.rs index 214f232..aad6f31 100644 --- a/src/commands/accounts.rs +++ b/src/commands/accounts.rs @@ -6,7 +6,7 @@ use crate::{cli::AccountsArgs, services::AuthService, AppCtx}; use bourso_api::account::{Account, AccountKind}; pub async fn handle(args: AccountsArgs, ctx: &AppCtx) -> Result<()> { - let auth_service = AuthService::with_defaults(&*ctx.settings_store); + let auth_service = AuthService::with_defaults(ctx.settings_store.as_ref()); let Some(client) = auth_service.login().await? else { return Ok(()); diff --git a/src/commands/trade/order.rs b/src/commands/trade/order.rs index a878e80..e823889 100644 --- a/src/commands/trade/order.rs +++ b/src/commands/trade/order.rs @@ -25,7 +25,7 @@ pub async fn handle(args: OrderArgs, ctx: &AppCtx) -> Result<()> { } async fn new_order(args: OrderNewArgs, ctx: &AppCtx) -> Result<()> { - let auth = AuthService::with_defaults(&*ctx.settings_store); + let auth = AuthService::with_defaults(ctx.settings_store.as_ref()); let Some(client) = auth.login().await? else { return Ok(()); diff --git a/src/commands/transfer.rs b/src/commands/transfer.rs index 73d729f..50fbfb5 100644 --- a/src/commands/transfer.rs +++ b/src/commands/transfer.rs @@ -7,7 +7,7 @@ use crate::{cli::TransferArgs, services::AuthService, ux::progress::TextProgress use bourso_api::client::transfer::TransferProgress; pub async fn handle(args: TransferArgs, ctx: &AppCtx) -> Result<()> { - let auth_service = AuthService::with_defaults(&*ctx.settings_store); + let auth_service = AuthService::with_defaults(ctx.settings_store.as_ref()); let Some(client) = auth_service.login().await? else { return Ok(()); diff --git a/src/services/auth.rs b/src/services/auth.rs index 99e4fce..78d2356 100644 --- a/src/services/auth.rs +++ b/src/services/auth.rs @@ -7,26 +7,25 @@ use bourso_api::{ types::{ClientNumber, MfaCode, Password}, }; -// TODO: fix naming, too many mismatches with customer_id / username / client_id / client_number // TODO: does it make sense to have MFA handling in the CLI? -pub trait CredentialsProvider: Send + Sync { - fn read_password(&self, prompt: &str) -> Result; - fn read_mfa_code(&self, prompt: &str) -> Result; +pub trait CredentialsProvider { + fn read_password(&self) -> Result; + fn read_mfa_code(&self) -> Result; } pub struct StdinCredentialsProvider; impl CredentialsProvider for StdinCredentialsProvider { - fn read_password(&self, prompt: &str) -> Result { - println!("{prompt}"); + fn read_password(&self) -> Result { + print!("Enter your password (hidden): "); Ok(rpassword::read_password()?.try_into()?) } - fn read_mfa_code(&self, prompt: &str) -> Result { - println!("{prompt}"); + fn read_mfa_code(&self) -> Result { + print!("Enter your MFA code (hidden): "); Ok(rpassword::read_password()?.try_into()?) } } -pub trait ClientFactory: Send + Sync { +pub trait ClientFactory { fn new_client(&self) -> BoursoWebClient; } pub struct DefaultClientFactory; @@ -65,31 +64,30 @@ impl<'a> AuthService<'a> { pub async fn login(&self) -> Result> { let settings = self.settings_store.load()?; - let Some(client_number) = settings.client_number.clone() else { + let Some(client_number) = settings.client_number.as_ref() else { warn!("No client number found in settings, please run `bourso config` to set it"); return Ok(None); }; info!( "We'll try to log you in with your customer id: {:?}", - client_number.as_str() + client_number.as_ref() ); info!("If you want to change it, you can run `bourso config` to set it"); println!(); let password = match settings.password.as_ref() { - Some(password) => password.clone(), + Some(password) => password, None => { info!("We'll need your password to log you in. It will not be stored."); - self.credentials_provider - .read_password("Enter your password (hidden):")? + &self.credentials_provider.read_password()? } }; let mut client = self.client_factory.new_client(); client.init_session().await?; match client - .login(client_number.as_str(), password.as_str()) + .login(client_number.as_ref(), password.as_ref()) .await { Ok(_) => { @@ -98,7 +96,7 @@ impl<'a> AuthService<'a> { } Err(e) => { if let Some(ClientError::MfaRequired) = e.downcast_ref::() { - self.handle_mfa(client, &client_number, &password).await + self.handle_mfa(client, client_number, password).await } else { Err(e) } @@ -118,19 +116,17 @@ impl<'a> AuthService<'a> { warn!("MFA threshold reached. Reinitializing session and logging in again."); client.init_session().await?; client - .login(client_number.as_str(), password.as_str()) + .login(client_number.as_ref(), password.as_ref()) .await?; info!("Login successful ✅"); return Ok(Some(client)); } let (otp_id, token_form, mfa_type) = client.request_mfa().await?; - let code = self - .credentials_provider - .read_mfa_code("Enter your MFA code (hidden):")?; + let code = &self.credentials_provider.read_mfa_code()?; match client - .submit_mfa(mfa_type, otp_id, code.as_str().to_string(), token_form) + .submit_mfa(mfa_type, otp_id, code.as_ref().to_string(), token_form) .await { Ok(_) => { From 11ae4f06456b560ee21e4587cd569d0c2dced44a Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Thu, 20 Nov 2025 02:25:51 +0000 Subject: [PATCH 46/49] refactor: integrate derive_more for enhanced type conversions and streamline argument handling in commands --- Cargo.lock | 21 ++++++ src/bourso_api/Cargo.toml | 1 + src/bourso_api/src/types.rs | 125 ++++++------------------------------ src/cli.rs | 4 +- src/commands/quote.rs | 2 +- src/commands/trade/order.rs | 4 +- src/commands/transfer.rs | 6 +- src/services/auth.rs | 6 +- 8 files changed, 52 insertions(+), 117 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cf9e09f..4580e22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -149,6 +149,7 @@ dependencies = [ "chrono", "clap", "cookie_store", + "derive_more", "futures-util", "lazy_static", "regex", @@ -316,6 +317,26 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "directories" version = "6.0.0" diff --git a/src/bourso_api/Cargo.toml b/src/bourso_api/Cargo.toml index 20aedc7..83f21da 100644 --- a/src/bourso_api/Cargo.toml +++ b/src/bourso_api/Cargo.toml @@ -20,6 +20,7 @@ tracing = { version = "0.1.41" } futures-util = { version = "0.3.31" } async-stream = { version = "0.3.6" } thiserror = { version = "2.0.17" } +derive_more = { version = "2.0.1", features = ["from", "into", "as_ref"] } [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] } diff --git a/src/bourso_api/src/types.rs b/src/bourso_api/src/types.rs index 5aa561f..2c40b3c 100644 --- a/src/bourso_api/src/types.rs +++ b/src/bourso_api/src/types.rs @@ -1,3 +1,5 @@ +use clap::ValueEnum; +use derive_more::{AsRef, From, Into}; use serde::{Deserialize, Serialize}; use std::str::FromStr; use thiserror::Error; @@ -16,8 +18,6 @@ pub enum ValueError { MoneyAmount, #[error("invalid transfer reason: must be 0-50 letters only (a-z, A-Z)")] TransferReason, - #[error("invalid quote length: must be one of: 1, 5, 30, 90, 180, 365, 1825, 3650")] - QuoteLength, #[error("invalid quote period: must be 0")] QuotePeriod, #[error("invalid mfa code: must be 6-12 digits (0-9)")] @@ -26,7 +26,7 @@ pub enum ValueError { Password, } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, AsRef, From, Into)] #[serde(try_from = "String", into = "String")] pub struct ClientNumber(String); impl ClientNumber { @@ -38,9 +38,6 @@ impl ClientNumber { Err(ValueError::ClientNumber) } } - pub fn as_str(&self) -> &str { - &self.0 - } } impl FromStr for ClientNumber { type Err = ValueError; @@ -48,24 +45,8 @@ impl FromStr for ClientNumber { Self::new(s) } } -impl AsRef for ClientNumber { - fn as_ref(&self) -> &str { - &self.0 - } -} -impl TryFrom for ClientNumber { - type Error = ValueError; - fn try_from(value: String) -> Result { - Self::new(&value) - } -} -impl From for String { - fn from(value: ClientNumber) -> Self { - value.0 - } -} -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, AsRef, From, Into)] pub struct AccountId(String); impl AccountId { pub fn new(s: &str) -> Result { @@ -76,9 +57,6 @@ impl AccountId { Err(ValueError::AccountId) } } - pub fn as_str(&self) -> &str { - &self.0 - } } impl FromStr for AccountId { type Err = ValueError; @@ -86,13 +64,8 @@ impl FromStr for AccountId { Self::new(s) } } -impl AsRef for AccountId { - fn as_ref(&self) -> &str { - &self.0 - } -} -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, AsRef, From, Into)] pub struct SymbolId(String); impl SymbolId { pub fn new(s: &str) -> Result { @@ -103,9 +76,6 @@ impl SymbolId { Err(ValueError::SymbolId) } } - pub fn as_str(&self) -> &str { - &self.0 - } } impl FromStr for SymbolId { type Err = ValueError; @@ -113,13 +83,8 @@ impl FromStr for SymbolId { Self::new(s) } } -impl AsRef for SymbolId { - fn as_ref(&self) -> &str { - &self.0 - } -} -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, AsRef, From, Into)] pub struct OrderQuantity(u64); impl OrderQuantity { pub fn new(v: u64) -> Result { @@ -163,7 +128,7 @@ impl FromStr for MoneyAmount { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, AsRef, From, Into)] pub struct TransferReason(String); impl TransferReason { pub fn new(s: &str) -> Result { @@ -176,9 +141,6 @@ impl TransferReason { } Ok(Self(t.into())) } - pub fn as_str(&self) -> &str { - &self.0 - } } impl FromStr for TransferReason { type Err = ValueError; @@ -186,21 +148,24 @@ impl FromStr for TransferReason { Self::new(s) } } -impl AsRef for TransferReason { - fn as_ref(&self) -> &str { - &self.0 - } -} -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)] pub enum QuoteLength { + #[value(name = "1")] D1, + #[value(name = "5")] D5, + #[value(name = "30")] D30, + #[value(name = "90")] D90, + #[value(name = "180")] D180, + #[value(name = "365")] D365, + #[value(name = "1825")] D1825, + #[value(name = "3650")] D3650, } impl QuoteLength { @@ -217,22 +182,6 @@ impl QuoteLength { } } } -impl FromStr for QuoteLength { - type Err = ValueError; - fn from_str(s: &str) -> Result { - match s.trim().parse::() { - Ok(1) => Ok(QuoteLength::D1), - Ok(5) => Ok(QuoteLength::D5), - Ok(30) => Ok(QuoteLength::D30), - Ok(90) => Ok(QuoteLength::D90), - Ok(180) => Ok(QuoteLength::D180), - Ok(365) => Ok(QuoteLength::D365), - Ok(1825) => Ok(QuoteLength::D1825), - Ok(3650) => Ok(QuoteLength::D3650), - _ => Err(ValueError::QuoteLength), - } - } -} // TODO: add support for other periods #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -257,7 +206,7 @@ impl FromStr for QuotePeriod { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, AsRef, From, Into)] pub struct MfaCode(String); impl MfaCode { pub fn new(s: &str) -> Result { @@ -268,9 +217,6 @@ impl MfaCode { Err(ValueError::MfaCode) } } - pub fn as_str(&self) -> &str { - &self.0 - } } impl FromStr for MfaCode { type Err = ValueError; @@ -278,24 +224,8 @@ impl FromStr for MfaCode { Self::new(s) } } -impl AsRef for MfaCode { - fn as_ref(&self) -> &str { - &self.0 - } -} -impl TryFrom for MfaCode { - type Error = ValueError; - fn try_from(value: String) -> Result { - Self::new(&value) - } -} -impl From for String { - fn from(value: MfaCode) -> Self { - value.0 - } -} -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, AsRef, From, Into)] #[serde(try_from = "String", into = "String")] pub struct Password(String); impl Password { @@ -307,9 +237,6 @@ impl Password { Err(ValueError::Password) } } - pub fn as_str(&self) -> &str { - &self.0 - } } impl FromStr for Password { type Err = ValueError; @@ -317,19 +244,3 @@ impl FromStr for Password { Self::new(s) } } -impl AsRef for Password { - fn as_ref(&self) -> &str { - &self.0 - } -} -impl TryFrom for Password { - type Error = ValueError; - fn try_from(value: String) -> Result { - Self::new(&value) - } -} -impl From for String { - fn from(value: Password) -> Self { - value.0 - } -} diff --git a/src/cli.rs b/src/cli.rs index eb08478..842df10 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -128,7 +128,7 @@ pub struct QuoteArgs { #[arg(long, value_name = "ID", value_parser = value_parser!(SymbolId))] pub symbol: SymbolId, - /// Length period of the stock (1, 5, 30, 90, 180, 365, 1825, 3650) + /// Length period of the stock #[arg( long, default_value = "30", @@ -136,7 +136,7 @@ pub struct QuoteArgs { )] pub length: QuoteLength, - /// Period of the stock (use "0" for default) + /// Period of the stock #[arg(long, default_value = "0", value_parser = value_parser!(QuotePeriod))] pub period: QuotePeriod, diff --git a/src/commands/quote.rs b/src/commands/quote.rs index 5ee0f3a..9d0d379 100644 --- a/src/commands/quote.rs +++ b/src/commands/quote.rs @@ -9,7 +9,7 @@ pub async fn handle(args: QuoteArgs) -> Result<()> { let client = bourso_api::get_client(); let quotes = client .get_ticks( - args.symbol.as_str(), + args.symbol.as_ref(), args.length.days(), args.period.value(), ) diff --git a/src/commands/trade/order.rs b/src/commands/trade/order.rs index e823889..bc0135a 100644 --- a/src/commands/trade/order.rs +++ b/src/commands/trade/order.rs @@ -35,7 +35,7 @@ async fn new_order(args: OrderNewArgs, ctx: &AppCtx) -> Result<()> { let accounts = client.get_accounts(Some(AccountKind::Trading)).await?; let account = accounts .iter() - .find(|a| a.id == args.account.as_str()) // TODO: compare AccountId instead of String + .find(|a| a.id == args.account.as_ref().as_str()) // TODO: compare AccountId instead of String .context("Account not found. Are you sure you have access to it? Run `bourso accounts` to list your accounts")?; let side: OrderSide = args.side; @@ -43,7 +43,7 @@ async fn new_order(args: OrderNewArgs, ctx: &AppCtx) -> Result<()> { let symbol = args.symbol; let _ = client - .order(side, account, symbol.as_str(), quantity, None) + .order(side, account, symbol.as_ref(), quantity, None) .await?; info!("Order submitted ✅"); diff --git a/src/commands/transfer.rs b/src/commands/transfer.rs index 50fbfb5..2e23808 100644 --- a/src/commands/transfer.rs +++ b/src/commands/transfer.rs @@ -17,19 +17,19 @@ pub async fn handle(args: TransferArgs, ctx: &AppCtx) -> Result<()> { let from_account = accounts .iter() - .find(|a| a.id == args.from_account.as_str()) // TODO: compare AccountId instead of String + .find(|a| a.id == args.from_account.as_ref().as_str()) // TODO: compare AccountId instead of String .context("From account not found. Are you sure you have access to it? Run `bourso accounts` to list your accounts")?; let to_account = accounts .iter() - .find(|a| a.id == args.to_account.as_str()) // TODO: compare AccountId instead of String + .find(|a| a.id == args.to_account.as_ref().as_str()) // TODO: compare AccountId instead of String .context("To account not found. Are you sure you have access to it? Run `bourso accounts` to list your accounts")?; let stream = client.transfer_funds( args.amount.get(), from_account.clone(), to_account.clone(), - args.reason.map(|r| r.as_str().to_string()), + args.reason.map(|r| r.as_ref().to_string()), ); let bar = TextProgressBar::new(30usize); diff --git a/src/services/auth.rs b/src/services/auth.rs index 78d2356..137754b 100644 --- a/src/services/auth.rs +++ b/src/services/auth.rs @@ -17,11 +17,13 @@ pub struct StdinCredentialsProvider; impl CredentialsProvider for StdinCredentialsProvider { fn read_password(&self) -> Result { print!("Enter your password (hidden): "); - Ok(rpassword::read_password()?.try_into()?) + let password = Password::new(&rpassword::read_password()?)?; + Ok(password) } fn read_mfa_code(&self) -> Result { print!("Enter your MFA code (hidden): "); - Ok(rpassword::read_password()?.try_into()?) + let mfa_code = MfaCode::new(&rpassword::read_password()?)?; + Ok(mfa_code) } } From 322510f0e381e1a6e78631518c3db76555868b6b Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Thu, 20 Nov 2025 02:57:23 +0000 Subject: [PATCH 47/49] refactor: introduce OrderSide enum and streamline order argument handling in CLI --- src/bourso_api/src/client/trade/order.rs | 10 +--------- src/bourso_api/src/types.rs | 10 +++++++++- src/cli.rs | 19 ++++++------------- src/commands/trade/order.rs | 2 +- 4 files changed, 17 insertions(+), 24 deletions(-) diff --git a/src/bourso_api/src/client/trade/order.rs b/src/bourso_api/src/client/trade/order.rs index 9df813c..41745e4 100644 --- a/src/bourso_api/src/client/trade/order.rs +++ b/src/bourso_api/src/client/trade/order.rs @@ -6,6 +6,7 @@ use tracing::{debug, info}; use crate::{ account::{Account, AccountKind}, client::config::Config, + types::OrderSide, }; use super::{get_trading_base_url, BoursoWebClient}; @@ -526,15 +527,6 @@ pub enum OrderKind { TradeAtLast, } -#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default, clap::ValueEnum)] -pub enum OrderSide { - #[default] - #[serde(rename = "B")] - Buy, - #[serde(rename = "S")] - Sell, -} - /// Order data submitted to the `/ordersimple/check` endpoint #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] pub struct OrderData { diff --git a/src/bourso_api/src/types.rs b/src/bourso_api/src/types.rs index 2c40b3c..d6af432 100644 --- a/src/bourso_api/src/types.rs +++ b/src/bourso_api/src/types.rs @@ -183,7 +183,15 @@ impl QuoteLength { } } -// TODO: add support for other periods +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, ValueEnum)] +pub enum OrderSide { + #[serde(rename = "B")] + Buy, + #[serde(rename = "S")] + Sell, +} + +// TODO: support only 0 period for now, add support for other periods #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct QuotePeriod(i64); impl QuotePeriod { diff --git a/src/cli.rs b/src/cli.rs index 842df10..ea90595 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,12 +1,9 @@ use clap::{value_parser, Args, Parser, Subcommand}; use std::path::PathBuf; -use bourso_api::{ - client::trade::order::OrderSide, - types::{ - AccountId, ClientNumber, MoneyAmount, OrderQuantity, QuoteLength, QuotePeriod, SymbolId, - TransferReason, - }, +use bourso_api::types::{ + AccountId, ClientNumber, MoneyAmount, OrderQuantity, OrderSide, QuoteLength, QuotePeriod, + SymbolId, TransferReason, }; // TODO: add debug option @@ -106,8 +103,8 @@ pub struct OrderNewArgs { #[arg(short, long, value_name = "ID", value_parser = value_parser!(AccountId))] pub account: AccountId, - /// Side of the order (buy/sell) - #[arg(long, value_parser = value_parser!(OrderSide))] + /// Side of the order + #[arg(long, default_value = "buy")] pub side: OrderSide, /// Symbol ID of the order (e.g: "1rTCW8") @@ -129,11 +126,7 @@ pub struct QuoteArgs { pub symbol: SymbolId, /// Length period of the stock - #[arg( - long, - default_value = "30", - value_parser = value_parser!(QuoteLength) - )] + #[arg(long, default_value = "30")] pub length: QuoteLength, /// Period of the stock diff --git a/src/commands/trade/order.rs b/src/commands/trade/order.rs index bc0135a..7c92171 100644 --- a/src/commands/trade/order.rs +++ b/src/commands/trade/order.rs @@ -8,7 +8,7 @@ use crate::{ }; use bourso_api::account::AccountKind; -use bourso_api::client::trade::order::OrderSide; +use bourso_api::types::OrderSide; pub async fn handle(args: OrderArgs, ctx: &AppCtx) -> Result<()> { match args.command { From b6767f170db3397514400c954f2fa906ec32e952 Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Thu, 20 Nov 2025 03:37:13 +0000 Subject: [PATCH 48/49] fix: read password issue --- src/services/auth.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/services/auth.rs b/src/services/auth.rs index 137754b..0a8bba7 100644 --- a/src/services/auth.rs +++ b/src/services/auth.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use std::io::{stdout, Write}; use tracing::{info, warn}; use crate::settings::SettingsStore; @@ -16,13 +17,17 @@ pub trait CredentialsProvider { pub struct StdinCredentialsProvider; impl CredentialsProvider for StdinCredentialsProvider { fn read_password(&self) -> Result { - print!("Enter your password (hidden): "); + print!("\nEnter your password (hidden): "); + let _ = stdout().flush(); let password = Password::new(&rpassword::read_password()?)?; + println!(); Ok(password) } fn read_mfa_code(&self) -> Result { - print!("Enter your MFA code (hidden): "); + print!("\nEnter your MFA code (hidden): "); + let _ = stdout().flush(); let mfa_code = MfaCode::new(&rpassword::read_password()?)?; + println!(); Ok(mfa_code) } } From b98de7000551871863e95fd38c0decc4bc5bb86c Mon Sep 17 00:00:00 2001 From: Mathious6 Date: Thu, 20 Nov 2025 03:41:01 +0000 Subject: [PATCH 49/49] refactor: renamed `consts` to `constants` --- src/settings/{consts.rs => constants.rs} | 0 src/settings/logging.rs | 2 +- src/settings/mod.rs | 2 +- src/settings/store.rs | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename src/settings/{consts.rs => constants.rs} (100%) diff --git a/src/settings/consts.rs b/src/settings/constants.rs similarity index 100% rename from src/settings/consts.rs rename to src/settings/constants.rs diff --git a/src/settings/logging.rs b/src/settings/logging.rs index c64854e..9d8db4b 100644 --- a/src/settings/logging.rs +++ b/src/settings/logging.rs @@ -12,7 +12,7 @@ use tracing_subscriber::{ registry, EnvFilter, }; -use crate::settings::consts::{ +use crate::settings::constants::{ APP_NAME, APP_ORGANIZATION, APP_QUALIFIER, DEFAULT_LOG_LEVEL, LOG_FILE, }; diff --git a/src/settings/mod.rs b/src/settings/mod.rs index 0b6f10e..e1fdc86 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -1,4 +1,4 @@ -mod consts; +mod constants; mod logging; mod store; diff --git a/src/settings/store.rs b/src/settings/store.rs index 391ffc3..fcbbc11 100644 --- a/src/settings/store.rs +++ b/src/settings/store.rs @@ -9,7 +9,7 @@ use std::{ path::PathBuf, }; -use crate::settings::consts::{APP_NAME, APP_ORGANIZATION, APP_QUALIFIER, SETTINGS_FILE}; +use crate::settings::constants::{APP_NAME, APP_ORGANIZATION, APP_QUALIFIER, SETTINGS_FILE}; #[derive(Serialize, Deserialize, Default)] pub struct Settings {