From 32b70c44c63a85e6d5216bd9286e6e0d94d3c98a Mon Sep 17 00:00:00 2001 From: h7x4 Date: Wed, 3 Dec 2025 14:58:43 +0900 Subject: [PATCH] client: print only json for `show-db`/`show-user`/`show-privs` `--json` The earlier version would print out human readable errors before printing the json, which was not ideal. This version prints out the errors inside the json. --- src/client/commands/show_db.rs | 43 +++------- src/client/commands/show_privs.rs | 67 ++++----------- src/client/commands/show_user.rs | 51 +++-------- src/core/protocol/commands/list_databases.rs | 53 ++++++++++++ src/core/protocol/commands/list_privileges.rs | 86 ++++++++++++++++++- src/core/protocol/commands/list_users.rs | 68 +++++++++++++++ 6 files changed, 247 insertions(+), 121 deletions(-) diff --git a/src/client/commands/show_db.rs b/src/client/commands/show_db.rs index 9a72087..939f6ab 100644 --- a/src/client/commands/show_db.rs +++ b/src/client/commands/show_db.rs @@ -1,14 +1,16 @@ use clap::Parser; use clap_complete::ArgValueCompleter; use futures_util::SinkExt; -use prettytable::{Cell, Row, Table}; use tokio_stream::StreamExt; use crate::{ client::commands::erroneous_server_response, core::{ completion::mysql_database_completer, - protocol::{ClientToServerMessageStream, Request, Response}, + protocol::{ + ClientToServerMessageStream, Request, Response, print_list_databases_output_status, + print_list_databases_output_status_json, + }, types::MySQLDatabase, }, }; @@ -40,25 +42,13 @@ pub async fn show_databases( server_connection.send(message).await?; - // TODO: collect errors for json output. - - let mut contained_errors = false; - let database_list = match server_connection.next().await { - Some(Ok(Response::ListDatabases(databases))) => databases - .into_iter() - .filter_map(|(database_name, result)| match result { - Ok(database_row) => Some(database_row), - Err(err) => { - contained_errors = true; - eprintln!("{}", err.to_error_message(&database_name)); - eprintln!("Skipping..."); - println!(); - None - } - }) - .collect::>(), + let databases = match server_connection.next().await { + Some(Ok(Response::ListDatabases(databases))) => databases, Some(Ok(Response::ListAllDatabases(database_list))) => match database_list { - Ok(list) => list, + Ok(list) => list + .into_iter() + .map(|db| (db.database.clone(), Ok(db))) + .collect(), Err(err) => { server_connection.send(Request::Exit).await?; return Err( @@ -72,19 +62,12 @@ pub async fn show_databases( server_connection.send(Request::Exit).await?; if args.json { - println!("{}", serde_json::to_string_pretty(&database_list)?); - } else if database_list.is_empty() { - println!("No databases to show."); + print_list_databases_output_status_json(&databases); } else { - let mut table = Table::new(); - table.add_row(Row::new(vec![Cell::new("Database")])); - for db in database_list { - table.add_row(row![db.database]); - } - table.printstd(); + print_list_databases_output_status(&databases); } - if args.fail && contained_errors { + if args.fail && databases.values().any(|res| res.is_err()) { std::process::exit(1); } diff --git a/src/client/commands/show_privs.rs b/src/client/commands/show_privs.rs index 6210e58..cee76b7 100644 --- a/src/client/commands/show_privs.rs +++ b/src/client/commands/show_privs.rs @@ -1,16 +1,17 @@ use clap::Parser; use clap_complete::ArgValueCompleter; use futures_util::SinkExt; -use prettytable::{Cell, Row, Table}; +use itertools::Itertools; use tokio_stream::StreamExt; use crate::{ client::commands::erroneous_server_response, core::{ - common::yn, completion::mysql_database_completer, - database_privileges::{DATABASE_PRIVILEGE_FIELDS, db_priv_field_human_readable_name}, - protocol::{ClientToServerMessageStream, Request, Response}, + protocol::{ + ClientToServerMessageStream, Request, Response, print_list_privileges_output_status, + print_list_privileges_output_status_json, + }, types::MySQLDatabase, }, }; @@ -41,24 +42,16 @@ pub async fn show_database_privileges( }; server_connection.send(message).await?; - let mut contained_errors = false; let privilege_data = match server_connection.next().await { - Some(Ok(Response::ListPrivileges(databases))) => databases - .into_iter() - .filter_map(|(database_name, result)| match result { - Ok(privileges) => Some(privileges), - Err(err) => { - contained_errors = true; - eprintln!("{}", err.to_error_message(&database_name)); - eprintln!("Skipping..."); - println!(); - None - } - }) - .flatten() - .collect::>(), + Some(Ok(Response::ListPrivileges(databases))) => databases, Some(Ok(Response::ListAllPrivileges(privilege_rows))) => match privilege_rows { - Ok(list) => list, + Ok(list) => list + .into_iter() + .map(|row| (row.db.clone(), row)) + .into_group_map() + .into_iter() + .map(|(db, rows)| (db, Ok(rows))) + .collect(), Err(err) => { server_connection.send(Request::Exit).await?; return Err(anyhow::anyhow!(err.to_error_message()) @@ -71,40 +64,12 @@ pub async fn show_database_privileges( server_connection.send(Request::Exit).await?; if args.json { - println!("{}", serde_json::to_string_pretty(&privilege_data)?); - } else if privilege_data.is_empty() { - println!("No database privileges to show."); + print_list_privileges_output_status_json(&privilege_data); } else { - let mut table = Table::new(); - table.add_row(Row::new( - DATABASE_PRIVILEGE_FIELDS - .into_iter() - .map(db_priv_field_human_readable_name) - .map(|name| Cell::new(&name)) - .collect(), - )); - - for row in privilege_data { - table.add_row(row![ - row.db, - row.user, - c->yn(row.select_priv), - c->yn(row.insert_priv), - c->yn(row.update_priv), - c->yn(row.delete_priv), - c->yn(row.create_priv), - c->yn(row.drop_priv), - c->yn(row.alter_priv), - c->yn(row.index_priv), - c->yn(row.create_tmp_table_priv), - c->yn(row.lock_tables_priv), - c->yn(row.references_priv), - ]); - } - table.printstd(); + print_list_privileges_output_status(&privilege_data); } - if args.fail && contained_errors { + if args.fail && privilege_data.values().any(|res| res.is_err()) { std::process::exit(1); } diff --git a/src/client/commands/show_user.rs b/src/client/commands/show_user.rs index ecdde64..566efb9 100644 --- a/src/client/commands/show_user.rs +++ b/src/client/commands/show_user.rs @@ -1,4 +1,3 @@ -use anyhow::Context; use clap::Parser; use clap_complete::ArgValueCompleter; use futures_util::SinkExt; @@ -8,7 +7,10 @@ use crate::{ client::commands::erroneous_server_response, core::{ completion::mysql_user_completer, - protocol::{ClientToServerMessageStream, Request, Response}, + protocol::{ + ClientToServerMessageStream, Request, Response, print_list_users_output_status, + print_list_users_output_status_json, + }, types::MySQLUser, }, }; @@ -43,22 +45,13 @@ pub async fn show_users( anyhow::bail!(err); } - let mut contained_errors = false; let users = match server_connection.next().await { - Some(Ok(Response::ListUsers(users))) => users - .into_iter() - .filter_map(|(username, result)| match result { - Ok(user) => Some(user), - Err(err) => { - contained_errors = true; - eprintln!("{}", err.to_error_message(&username)); - eprintln!("Skipping..."); - None - } - }) - .collect::>(), + Some(Ok(Response::ListUsers(users))) => users, Some(Ok(Response::ListAllUsers(users))) => match users { - Ok(users) => users, + Ok(users) => users + .into_iter() + .map(|user| (user.user.clone(), Ok(user))) + .collect(), Err(err) => { server_connection.send(Request::Exit).await?; return Err( @@ -72,32 +65,12 @@ pub async fn show_users( server_connection.send(Request::Exit).await?; if args.json { - println!( - "{}", - serde_json::to_string_pretty(&users).context("Failed to serialize users to JSON")? - ); - } else if users.is_empty() { - println!("No users to show."); + print_list_users_output_status_json(&users); } else { - let mut table = prettytable::Table::new(); - table.add_row(row![ - "User", - "Password is set", - "Locked", - "Databases where user has privileges" - ]); - for user in users { - table.add_row(row![ - user.user, - user.has_password, - user.is_locked, - user.databases.join("\n") - ]); - } - table.printstd(); + print_list_users_output_status(&users); } - if args.fail && contained_errors { + if args.fail && users.values().any(|result| result.is_err()) { std::process::exit(1); } diff --git a/src/core/protocol/commands/list_databases.rs b/src/core/protocol/commands/list_databases.rs index 2c36a5b..9b9badc 100644 --- a/src/core/protocol/commands/list_databases.rs +++ b/src/core/protocol/commands/list_databases.rs @@ -1,6 +1,8 @@ use std::collections::BTreeMap; +use prettytable::{Cell, Row, Table}; use serde::{Deserialize, Serialize}; +use serde_json::json; use crate::{ core::{ @@ -22,6 +24,57 @@ pub enum ListDatabasesError { MySqlError(String), } +pub fn print_list_databases_output_status(output: &ListDatabasesResponse) { + let mut final_database_list: Vec<&DatabaseRow> = Vec::new(); + for (db_name, db_result) in output { + match db_result { + Ok(db_row) => final_database_list.push(db_row), + Err(err) => { + eprintln!("{}", err.to_error_message(db_name)); + eprintln!("Skipping..."); + } + } + } + + if final_database_list.is_empty() { + println!("No databases to show."); + } else { + let mut table = Table::new(); + table.add_row(Row::new(vec![Cell::new("Database")])); + for db in final_database_list { + table.add_row(row![db.database]); + } + table.printstd(); + } +} + +pub fn print_list_databases_output_status_json(output: &ListDatabasesResponse) { + let value = output + .iter() + .map(|(name, result)| match result { + Ok(_row) => ( + name.to_string(), + json!({ + "status": "success", + // NOTE: there will likely be more data to include here in the future + }), + ), + Err(err) => ( + name.to_string(), + json!({ + "status": "error", + "error": err.to_error_message(name), + }), + ), + }) + .collect::>(); + println!( + "{}", + serde_json::to_string_pretty(&value) + .unwrap_or("Failed to serialize result to JSON".to_string()) + ); +} + impl ListDatabasesError { pub fn to_error_message(&self, database_name: &MySQLDatabase) -> String { match self { diff --git a/src/core/protocol/commands/list_privileges.rs b/src/core/protocol/commands/list_privileges.rs index ae96f36..25e8bb7 100644 --- a/src/core/protocol/commands/list_privileges.rs +++ b/src/core/protocol/commands/list_privileges.rs @@ -4,10 +4,16 @@ use std::collections::BTreeMap; +use itertools::Itertools; +use prettytable::{Cell, Row, Table}; use serde::{Deserialize, Serialize}; +use serde_json::json; use crate::core::{ - database_privileges::DatabasePrivilegeRow, + common::yn, + database_privileges::{ + DATABASE_PRIVILEGE_FIELDS, DatabasePrivilegeRow, db_priv_field_human_readable_name, + }, protocol::request_validation::{NameValidationError, OwnerValidationError}, types::{DbOrUser, MySQLDatabase}, }; @@ -17,6 +23,84 @@ pub type ListPrivilegesRequest = Option>; pub type ListPrivilegesResponse = BTreeMap, GetDatabasesPrivilegeDataError>>; +pub fn print_list_privileges_output_status(output: &ListPrivilegesResponse) { + let mut final_privs_map: BTreeMap> = BTreeMap::new(); + for (db_name, db_result) in output { + match db_result { + Ok(db_rows) => { + final_privs_map.insert(db_name.clone(), db_rows.clone()); + } + Err(err) => { + eprintln!("{}", err.to_error_message(db_name)); + eprintln!("Skipping..."); + } + } + } + + if final_privs_map.is_empty() { + println!("No privileges to show."); + } else { + let mut table = Table::new(); + + table.add_row(Row::new( + DATABASE_PRIVILEGE_FIELDS + .into_iter() + .map(db_priv_field_human_readable_name) + .map(|name| Cell::new(&name)) + .collect(), + )); + + for (_database, rows) in final_privs_map { + for row in rows.iter() { + table.add_row(row![ + row.db, + row.user, + c->yn(row.select_priv), + c->yn(row.insert_priv), + c->yn(row.update_priv), + c->yn(row.delete_priv), + c->yn(row.create_priv), + c->yn(row.drop_priv), + c->yn(row.alter_priv), + c->yn(row.index_priv), + c->yn(row.create_tmp_table_priv), + c->yn(row.lock_tables_priv), + c->yn(row.references_priv), + ]); + } + } + + table.printstd(); + } +} + +pub fn print_list_privileges_output_status_json(output: &ListPrivilegesResponse) { + let value = output + .iter() + .map(|(name, result)| match result { + Ok(row) => ( + name.to_string(), + json!({ + "status": "success", + "value": row.iter().into_group_map_by(|priv_row| priv_row.user.clone()), + }), + ), + Err(err) => ( + name.to_string(), + json!({ + "status": "error", + "error": err.to_error_message(name), + }), + ), + }) + .collect::>(); + println!( + "{}", + serde_json::to_string_pretty(&value) + .unwrap_or("Failed to serialize result to JSON".to_string()) + ); +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum GetDatabasesPrivilegeDataError { SanitizationError(NameValidationError), diff --git a/src/core/protocol/commands/list_users.rs b/src/core/protocol/commands/list_users.rs index d75b0a1..cea9a84 100644 --- a/src/core/protocol/commands/list_users.rs +++ b/src/core/protocol/commands/list_users.rs @@ -1,6 +1,8 @@ use std::collections::BTreeMap; +use prettytable::Table; use serde::{Deserialize, Serialize}; +use serde_json::json; use crate::{ core::{ @@ -22,6 +24,72 @@ pub enum ListUsersError { MySqlError(String), } +pub fn print_list_users_output_status(output: &ListUsersResponse) { + let mut final_user_list: Vec<&DatabaseUser> = Vec::new(); + for (db_name, db_result) in output { + match db_result { + Ok(db_row) => final_user_list.push(db_row), + Err(err) => { + eprintln!("{}", err.to_error_message(db_name)); + eprintln!("Skipping..."); + } + } + } + + if final_user_list.is_empty() { + println!("No users to show."); + } else { + let mut table = Table::new(); + table.add_row(row![ + "User", + "Password is set", + "Locked", + "Databases where user has privileges" + ]); + for user in final_user_list { + table.add_row(row![ + user.user, + user.has_password, + user.is_locked, + user.databases.join("\n") + ]); + } + table.printstd(); + } +} + +pub fn print_list_users_output_status_json(output: &ListUsersResponse) { + let value = output + .iter() + .map(|(name, result)| match result { + Ok(row) => ( + name.to_string(), + json!({ + "status": "success", + "value": { + "user": row.user, + "has_password": row.has_password, + "is_locked": row.is_locked, + "databases": row.databases, + } + }), + ), + Err(err) => ( + name.to_string(), + json!({ + "status": "error", + "error": err.to_error_message(name), + }), + ), + }) + .collect::>(); + println!( + "{}", + serde_json::to_string_pretty(&value) + .unwrap_or("Failed to serialize result to JSON".to_string()) + ); +} + impl ListUsersError { pub fn to_error_message(&self, username: &MySQLUser) -> String { match self {