client: print only json for show-db/show-user/show-privs --json
All checks were successful
Build and test / check (push) Successful in 1m52s
Build and test / build (push) Successful in 3m4s
Build and test / test (push) Successful in 3m28s
Build and test / check-license (push) Successful in 5m47s
Build and test / docs (push) Successful in 4m45s

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.
This commit is contained in:
2025-12-03 14:58:43 +09:00
parent 6a4a83367e
commit 32b70c44c6
6 changed files with 247 additions and 121 deletions

View File

@@ -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::<Vec<_>>(),
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);
}

View File

@@ -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::<Vec<_>>(),
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);
}

View File

@@ -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::<Vec<_>>(),
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);
}

View File

@@ -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::<serde_json::Map<_, _>>();
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 {

View File

@@ -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<Vec<MySQLDatabase>>;
pub type ListPrivilegesResponse =
BTreeMap<MySQLDatabase, Result<Vec<DatabasePrivilegeRow>, GetDatabasesPrivilegeDataError>>;
pub fn print_list_privileges_output_status(output: &ListPrivilegesResponse) {
let mut final_privs_map: BTreeMap<MySQLDatabase, Vec<DatabasePrivilegeRow>> = 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::<serde_json::Map<_, _>>();
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),

View File

@@ -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::<serde_json::Map<_, _>>();
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 {