diff --git a/src/commands/vault.rs b/src/commands/vault.rs index 2f6565915fbc399c6bb3a774d495456e1f00b5ba..cb8494587e24a2ac7ff28fb29570cadbc210f459 100644 --- a/src/commands/vault.rs +++ b/src/commands/vault.rs @@ -9,8 +9,8 @@ use crate::*; use age::secrecy::Secret; use anyhow::anyhow; use sea_orm::ActiveValue::Set; -use sea_orm::ModelTrait; use sea_orm::{ConnectionTrait, TransactionTrait}; +use sea_orm::{DatabaseConnection, ModelTrait}; use sp_core::crypto::AddressUri; use std::cell::RefCell; use std::io::{Read, Write}; @@ -187,7 +187,7 @@ pub struct AddressOrVaultNameGroup { vault_name: Option<String>, } -/// vault inspect for JSON serialization +/// vault inspect result data #[derive(Serialize)] struct VaultInspectData { substrate_uri: String, @@ -198,6 +198,29 @@ struct VaultInspectData { g1v1_public_key: Option<String>, } +// Manual human-readable implementation +impl SerializeHuman for VaultInspectData { + fn serialize_human(&self) -> String { + let mut lines = vec![ + format!("Substrate URI: '{}'", self.substrate_uri), + format!( + "Crypto scheme: {}", + <&'static str>::from(self.crypto_scheme) + ), + match &self.secret_seed { + None => "Secret seed/mini-secret: N/A".to_string(), + Some(seed) => format!("Secret seed/mini-secret: '0x{}'", seed), + }, + format!("Public key (hex): '0x{}'", self.public_key_hex), + format!("SS58 Address: '{}'", self.ss58_address), + ]; + if let Some(ref g1v1) = self.g1v1_public_key { + lines.push(format!("(potential G1v1 public key: '{}')", g1v1)); + } + lines.join("\n") + } +} + pub struct VaultDataToImport { secret_format: SecretFormat, secret_suri: String, @@ -641,64 +664,10 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE password, no_password, } => { - let is_interactive = password.is_none() && !no_password; - - let account_tree_node_to_inspect = - retrieve_account_tree_node(db, address_or_vault_name).await?; - - let base_account_tree_node = - vault_account::get_base_account_tree_node(&account_tree_node_to_inspect); - - let is_base_account = - Rc::ptr_eq(&account_tree_node_to_inspect, &base_account_tree_node); - - if is_interactive && !is_base_account { - let base_account = base_account_tree_node.borrow().account.clone(); - println!("The linked <Base> account is {base_account}"); - } - - // Handle password from non-interactive mode or ask for it - let password = if no_password { - String::new() - } else if let Some(password) = password { - password - } else { - println!("Enter password to decrypt the <Base> account key"); - inputs::prompt_password()? - }; - - let vault_inspect_data = - compute_vault_inspect_data(&account_tree_node_to_inspect, password)?; - - match data.args.output_format { - OutputFormat::Human => { - println!("Substrate URI: '{}'", vault_inspect_data.substrate_uri); - println!( - "Crypto scheme: {}", - <&'static str>::from(vault_inspect_data.crypto_scheme) - ); - match vault_inspect_data.secret_seed { - None => println!("Secret seed/mini-secret: N/A"), - Some(secret_seed) => { - println!("Secret seed/mini-secret: '0x{}'", secret_seed) - } - } - println!( - "Public key (hex): '0x{}'", - vault_inspect_data.public_key_hex - ); - println!("SS58 Address: '{}'", vault_inspect_data.ss58_address); - if let Some(g1v1_public_key) = vault_inspect_data.g1v1_public_key { - println!("(potential G1v1 public key: '{}')", g1v1_public_key); - } - } - OutputFormat::Json => { - println!( - "{}", - serde_json::to_string(&vault_inspect_data).map_err(|e| anyhow!(e))? - ); - } - } + print_command_output_async(data.args.output_format, || { + handle_inspect_command(db, address_or_vault_name, password, no_password) + }) + .await? } Subcommand::Migrate => { println!("Migrating existing key files to db"); @@ -789,6 +758,40 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE Ok(()) } +async fn handle_inspect_command( + db: &DatabaseConnection, + address_or_vault_name: AddressOrVaultNameGroup, + password: Option<String>, + no_password: bool, +) -> Result<VaultInspectData, GcliError> { + let is_interactive = password.is_none() && !no_password; + + let account_tree_node_to_inspect = + retrieve_account_tree_node(db, address_or_vault_name).await?; + + let base_account_tree_node = + vault_account::get_base_account_tree_node(&account_tree_node_to_inspect); + + let is_base_account = Rc::ptr_eq(&account_tree_node_to_inspect, &base_account_tree_node); + + if is_interactive && !is_base_account { + let base_account = base_account_tree_node.borrow().account.clone(); + println!("The linked <Base> account is {base_account}"); + } + + // Handle password from non-interactive mode or ask for it + let password = if no_password { + String::new() + } else if let Some(password) = password { + password + } else { + println!("Enter password to decrypt the <Base> account key"); + inputs::prompt_password()? + }; + + compute_vault_inspect_data(&account_tree_node_to_inspect, password) +} + fn compute_vault_inspect_data( account_tree_node_to_inspect: &Rc<RefCell<AccountTreeNode>>, password: String, diff --git a/src/main.rs b/src/main.rs index 8e417d6002ad0fefa59da870cc48aa307b4f7513..c29674e71da56dde30da36f7371de2efb6e3e289 100644 --- a/src/main.rs +++ b/src/main.rs @@ -82,7 +82,7 @@ pub struct Args { // TODO derive the fromstr implementation /// secret format #[derive(Clone, Copy, Debug, Eq, PartialEq, Default)] -enum OutputFormat { +pub enum OutputFormat { /// Human #[default] Human, @@ -114,6 +114,147 @@ impl From<OutputFormat> for OsStr { } } +pub trait SerializeJson { + fn serialize_json(&self) -> Result<String, GcliError>; +} + +// Blanket implementation for all Serialize types +impl<T> SerializeJson for T +where + T: Serialize, +{ + fn serialize_json(&self) -> Result<String, GcliError> { + serde_json::to_string(self).map_err(GcliError::JsonError) + } +} + +/// Structured Error for JSON OutputFormat to be serialized in JSON +/// +/// Purpose is to translate a GcliError into a JSON object +#[derive(serde::Serialize)] +pub struct JsonOutputError { + pub error_type: String, + pub error: String, +} + +impl GcliError { + pub fn error_type(&self) -> &'static str { + match self { + GcliError::Subxt(_) => "Subxt", + GcliError::Duniter(_) => "Duniter", + GcliError::Indexer(_) => "Indexer", + GcliError::DatabaseError(_) => "DatabaseError", + GcliError::Logic(_) => "Logic", + GcliError::Input(_) => "Input", + GcliError::Anyhow(_) => "Anyhow", + GcliError::IoError(_) => "IoError", + GcliError::JsonError(_) => "JsonError", + } + } + + pub fn error_message(&self) -> String { + match self { + GcliError::Subxt(e) => e.to_string(), + GcliError::DatabaseError(e) => e.to_string(), + GcliError::Anyhow(e) => e.to_string(), + GcliError::IoError(e) => e.to_string(), + GcliError::JsonError(e) => e.to_string(), + GcliError::Duniter(e) + | GcliError::Indexer(e) + | GcliError::Logic(e) + | GcliError::Input(e) => e.clone(), + } + } +} + +impl From<GcliError> for JsonOutputError { + fn from(gcli_error: GcliError) -> JsonOutputError { + JsonOutputError { + error_type: gcli_error.error_type().to_string(), + error: gcli_error.error_message(), + } + } +} + +pub trait SerializeHuman { + fn serialize_human(&self) -> String; +} + +// Shared logic +fn print_command_output_impl<T>( + format: OutputFormat, + result: Result<T, GcliError>, +) -> Result<(), GcliError> +where + T: SerializeJson + SerializeHuman, +{ + match format { + OutputFormat::Human => match result { + Ok(value) => { + println!("{}", value.serialize_human()); + Ok(()) + } + Err(e) => Err(e), + }, + OutputFormat::Json => match result { + Ok(value) => { + println!("{}", value.serialize_json()?); + Ok(()) + } + Err(e) => { + // create a JsonOutputError and serialize it to JSON (don't want to break the JSON format) + let json_error: JsonOutputError = e.into(); + println!("{}", serde_json::to_string(&json_error)?); + Ok(()) + } + }, + } +} + +/// Prints the output of a fallible computation in the specified format. +/// +/// This function takes an `OutputFormat` and a closure that returns a `Result<T, GcliError>`, +/// where `T` implements both `SerializeJson` and `SerializeHuman`. +/// +/// On success, it prints the value in either human-readable or JSON format. +/// +/// On error, it simply returns the GcliError for human output, or as a structured JSON error object for JSON output. +/// +/// # Arguments +/// * `format` - The desired output format (`Human` or `Json`). +/// * `f` - A closure that performs the computation and returns a `Result<T, GcliError>`. +/// +/// # Returns +/// * `Ok(())` if the output was printed successfully. +/// * `Err(GcliError)` if an error occurred and the output format is `Human`. +/// +/// # Example +/// ``` +/// print_output(OutputFormat::Json, || my_command_handler(some, params))?; +/// ``` +pub fn print_command_output<T, F>(format: OutputFormat, f: F) -> Result<(), GcliError> +where + T: SerializeJson + SerializeHuman, + F: FnOnce() -> Result<T, GcliError>, +{ + let result = f(); + print_command_output_impl(format, result) +} + +/// Check [`print_command_output`] for documentation +pub async fn print_command_output_async<T, F, Fut>( + format: OutputFormat, + f: F, +) -> Result<(), GcliError> +where + T: SerializeJson + SerializeHuman, + F: FnOnce() -> Fut, + Fut: std::future::Future<Output = Result<T, GcliError>>, +{ + let result = f().await; + print_command_output_impl(format, result) +} + /// define subcommands #[derive(Clone, Debug, clap::Subcommand, Default)] pub enum Subcommand { diff --git a/src/utils.rs b/src/utils.rs index 68ec0118e858a2cbae818b132772aa81cc2bacea..f931ca4280575c971e347f48c3d1cd9c4feac065 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -116,7 +116,11 @@ pub enum GcliError { /// error coming from io #[allow(dead_code)] IoError(std::io::Error), + /// error coming from serde_json + #[allow(dead_code)] + JsonError(serde_json::error::Error), } + impl std::fmt::Display for GcliError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { @@ -150,6 +154,11 @@ impl From<std::io::Error> for GcliError { GcliError::IoError(error) } } +impl From<serde_json::error::Error> for GcliError { + fn from(error: serde_json::error::Error) -> Self { + GcliError::JsonError(error) + } +} impl From<sea_orm::DbErr> for GcliError { fn from(error: sea_orm::DbErr) -> Self { GcliError::DatabaseError(error)