use crate::error::RedisCtlError;
use anyhow::Context;
use clap::Subcommand;
use crate::{cli::OutputFormat, connection::ConnectionManager, error::Result as CliResult};
#[allow(dead_code)]
pub async fn handle_usage_report_command(
conn_mgr: &ConnectionManager,
profile_name: Option<&str>,
usage_report_cmd: UsageReportCommands,
output_format: OutputFormat,
query: Option<&str>,
) -> CliResult<()> {
usage_report_cmd
.execute(conn_mgr, profile_name, output_format, query)
.await
}
#[derive(Debug, Clone, Subcommand)]
pub enum UsageReportCommands {
Get,
Export {
#[arg(short, long)]
output: String,
#[arg(short, long, default_value = "json")]
format: String,
},
}
impl UsageReportCommands {
#[allow(dead_code)]
pub async fn execute(
&self,
conn_mgr: &ConnectionManager,
profile_name: Option<&str>,
output_format: OutputFormat,
query: Option<&str>,
) -> CliResult<()> {
handle_usage_report_command_impl(conn_mgr, profile_name, self, output_format, query).await
}
}
#[allow(dead_code)]
async fn handle_usage_report_command_impl(
conn_mgr: &ConnectionManager,
profile_name: Option<&str>,
command: &UsageReportCommands,
output_format: OutputFormat,
query: Option<&str>,
) -> CliResult<()> {
let client = conn_mgr.create_enterprise_client(profile_name).await?;
match command {
UsageReportCommands::Get => {
let response: serde_json::Value = client
.get("/v1/usage_report")
.await
.map_err(RedisCtlError::from)?;
let output_data = if let Some(q) = query {
super::utils::apply_jmespath(&response, q)?
} else {
response
};
super::utils::print_formatted_output(output_data, output_format)?;
}
UsageReportCommands::Export { output, format } => {
let response: serde_json::Value = client
.get("/v1/usage_report")
.await
.map_err(RedisCtlError::from)?;
let output_data = if let Some(q) = query {
super::utils::apply_jmespath(&response, q)?
} else {
response
};
match format.as_str() {
"json" => {
let json_str = serde_json::to_string_pretty(&output_data)
.context("Failed to serialize to JSON")?;
std::fs::write(output, json_str)
.context(format!("Failed to write to {}", output))?;
println!("Usage report exported to {}", output);
}
"csv" => {
let csv_data = json_to_csv(&output_data)?;
std::fs::write(output, csv_data)
.context(format!("Failed to write to {}", output))?;
println!("Usage report exported to {} as CSV", output);
}
_ => {
return Err(anyhow::anyhow!(
"Unsupported format: {}. Use 'json' or 'csv'",
format
)
.into());
}
}
}
}
Ok(())
}
fn json_to_csv(data: &serde_json::Value) -> CliResult<String> {
let mut csv = String::new();
if let Some(obj) = data.as_object() {
let headers: Vec<String> = obj.keys().map(|k| k.to_string()).collect();
csv.push_str(&headers.join(","));
csv.push('\n');
let values: Vec<String> = obj
.values()
.map(|v| match v {
serde_json::Value::String(s) => format!("\"{}\"", s.replace('"', "\"\"")),
_ => v.to_string(),
})
.collect();
csv.push_str(&values.join(","));
csv.push('\n');
} else if let Some(arr) = data.as_array() {
if let Some(first) = arr.first()
&& let Some(obj) = first.as_object()
{
let headers: Vec<String> = obj.keys().map(|k| k.to_string()).collect();
csv.push_str(&headers.join(","));
csv.push('\n');
for item in arr {
if let Some(obj) = item.as_object() {
let values: Vec<String> = headers
.iter()
.map(|h| {
obj.get(h)
.map(|v| match v {
serde_json::Value::String(s) => {
format!("\"{}\"", s.replace('"', "\"\""))
}
_ => v.to_string(),
})
.unwrap_or_else(|| String::from(""))
})
.collect();
csv.push_str(&values.join(","));
csv.push('\n');
}
}
}
}
Ok(csv)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_usage_report_command_parsing() {
use clap::Parser;
#[derive(Parser)]
struct TestCli {
#[command(subcommand)]
cmd: UsageReportCommands,
}
let cli = TestCli::parse_from(["test", "get"]);
assert!(matches!(cli.cmd, UsageReportCommands::Get));
let cli = TestCli::parse_from(["test", "export", "--output", "report.json"]);
if let UsageReportCommands::Export { output, format } = cli.cmd {
assert_eq!(output, "report.json");
assert_eq!(format, "json");
} else {
panic!("Expected Export command");
}
let cli = TestCli::parse_from(["test", "export", "-o", "report.csv", "-f", "csv"]);
if let UsageReportCommands::Export { output, format } = cli.cmd {
assert_eq!(output, "report.csv");
assert_eq!(format, "csv");
} else {
panic!("Expected Export command");
}
}
#[test]
fn test_json_to_csv() {
let json = serde_json::json!({
"cluster": "test-cluster",
"databases": 5,
"memory_gb": 128
});
let csv = json_to_csv(&json).unwrap();
assert!(csv.contains("cluster,databases,memory_gb"));
assert!(csv.contains("\"test-cluster\",5,128"));
let json = serde_json::json!([
{"name": "db1", "memory": 1024},
{"name": "db2", "memory": 2048}
]);
let csv = json_to_csv(&json).unwrap();
assert!(csv.contains("memory,name") || csv.contains("name,memory"));
assert!(csv.contains("\"db1\""));
assert!(csv.contains("\"db2\""));
assert!(csv.contains("1024"));
assert!(csv.contains("2048"));
}
}