use crate::cache::models::CachedCommand;
use crate::cli::OutputFormat;
use crate::constants;
use crate::engine::executor::apply_jq_filter;
use crate::error::Error;
use crate::invocation::ExecutionResult;
use crate::utils::to_kebab_case;
use serde_json::Value;
use std::collections::BTreeMap;
use std::fmt::Write;
use tabled::Table;
const MAX_TABLE_ROWS: usize = 1000;
#[derive(tabled::Tabled)]
struct TableRow {
#[tabled(rename = "Key")]
key: String,
#[tabled(rename = "Value")]
value: String,
}
#[derive(tabled::Tabled)]
struct KeyValue {
#[tabled(rename = "Key")]
key: String,
#[tabled(rename = "Value")]
value: String,
}
pub fn render_result(
result: &ExecutionResult,
format: &OutputFormat,
jq_filter: Option<&str>,
) -> Result<(), Error> {
match result {
ExecutionResult::Success { body, .. } | ExecutionResult::Cached { body } => {
if body.is_empty() {
return Ok(());
}
format_and_print(body, format, jq_filter, false)?;
}
ExecutionResult::DryRun { request_info } => {
let output = serde_json::to_string_pretty(request_info).map_err(|e| {
Error::serialization_error(format!("Failed to serialize dry run info: {e}"))
})?;
println!("{output}");
}
ExecutionResult::Empty => {}
}
Ok(())
}
pub fn render_result_to_string(
result: &ExecutionResult,
format: &OutputFormat,
jq_filter: Option<&str>,
) -> Result<Option<String>, Error> {
match result {
ExecutionResult::Success { body, .. } | ExecutionResult::Cached { body } => {
if body.is_empty() {
return Ok(None);
}
format_and_print(body, format, jq_filter, true)
}
ExecutionResult::DryRun { request_info } => {
let output = serde_json::to_string_pretty(request_info).map_err(|e| {
Error::serialization_error(format!("Failed to serialize dry run info: {e}"))
})?;
Ok(Some(output))
}
ExecutionResult::Empty => Ok(None),
}
}
pub fn render_examples(operation: &CachedCommand) {
println!("Command: {}\n", to_kebab_case(&operation.operation_id));
if let Some(ref summary) = operation.summary {
println!("Description: {summary}\n");
}
println!("Method: {} {}\n", operation.method, operation.path);
if operation.examples.is_empty() {
println!("No examples available for this command.");
return;
}
println!("Examples:\n");
for (i, example) in operation.examples.iter().enumerate() {
println!("{}. {}", i + 1, example.description);
println!(" {}", example.command_line);
if let Some(ref explanation) = example.explanation {
println!(" {explanation}");
}
println!();
}
if operation.parameters.is_empty() {
return;
}
println!("Parameters:");
for param in &operation.parameters {
let required = if param.required { " (required)" } else { "" };
let param_type = param.schema_type.as_deref().unwrap_or("string");
println!(" --{}{} [{}]", param.name, required, param_type);
let Some(ref desc) = param.description else {
continue;
};
println!(" {desc}");
}
println!();
if operation.request_body.is_some() {
println!("Request Body:");
println!(" --body JSON (required)");
println!(" JSON data to send in the request body");
}
}
fn format_and_print(
response_text: &str,
output_format: &OutputFormat,
jq_filter: Option<&str>,
capture_output: bool,
) -> Result<Option<String>, Error> {
let processed_text = if let Some(filter) = jq_filter {
apply_jq_filter(response_text, filter)?
} else {
response_text.to_string()
};
match output_format {
OutputFormat::Json => {
let output = serde_json::from_str::<Value>(&processed_text)
.ok()
.and_then(|json_value| serde_json::to_string_pretty(&json_value).ok())
.unwrap_or_else(|| processed_text.clone());
if capture_output {
return Ok(Some(output));
}
println!("{output}");
}
OutputFormat::Yaml => {
let output = serde_json::from_str::<Value>(&processed_text)
.ok()
.and_then(|json_value| serde_yaml::to_string(&json_value).ok())
.unwrap_or_else(|| processed_text.clone());
if capture_output {
return Ok(Some(output));
}
println!("{output}");
}
OutputFormat::Table => {
let Ok(json_value) = serde_json::from_str::<Value>(&processed_text) else {
if capture_output {
return Ok(Some(processed_text));
}
println!("{processed_text}");
return Ok(None);
};
let table_output = print_as_table(&json_value, capture_output)?;
if capture_output {
return Ok(table_output);
}
}
}
Ok(None)
}
fn print_numbered_list(items: &[Value], capture_output: bool) -> Option<String> {
if capture_output {
let mut output = String::new();
for (i, item) in items.iter().enumerate() {
writeln!(&mut output, "{}: {}", i, format_value_for_table(item))
.expect("writing to String cannot fail");
}
return Some(output.trim_end().to_string());
}
for (i, item) in items.iter().enumerate() {
println!("{}: {}", i, format_value_for_table(item));
}
None
}
fn output_or_capture(message: &str, capture_output: bool) -> Option<String> {
if capture_output {
return Some(message.to_string());
}
println!("{message}");
None
}
#[allow(clippy::unnecessary_wraps, clippy::too_many_lines)]
fn print_as_table(json_value: &Value, capture_output: bool) -> Result<Option<String>, Error> {
match json_value {
Value::Array(items) => {
if items.is_empty() {
return Ok(output_or_capture(constants::EMPTY_ARRAY, capture_output));
}
if items.len() > MAX_TABLE_ROWS {
let msg = format!(
"Array too large: {} items (max {} for table display)\nUse --format json or --jq to process the full data",
items.len(),
MAX_TABLE_ROWS
);
return Ok(output_or_capture(&msg, capture_output));
}
let Some(Value::Object(_)) = items.first() else {
return Ok(print_numbered_list(items, capture_output));
};
let mut table_data: Vec<BTreeMap<String, String>> = Vec::new();
for item in items {
let Value::Object(obj) = item else {
continue;
};
let mut row = BTreeMap::new();
for (key, value) in obj {
row.insert(key.clone(), format_value_for_table(value));
}
table_data.push(row);
}
if table_data.is_empty() {
return Ok(print_numbered_list(items, capture_output));
}
let mut rows = Vec::new();
for (i, row) in table_data.iter().enumerate() {
if i > 0 {
rows.push(TableRow {
key: "---".to_string(),
value: "---".to_string(),
});
}
for (key, value) in row {
rows.push(TableRow {
key: key.clone(),
value: value.clone(),
});
}
}
let table = Table::new(&rows);
Ok(output_or_capture(&table.to_string(), capture_output))
}
Value::Object(obj) => {
if obj.len() > MAX_TABLE_ROWS {
let msg = format!(
"Object too large: {} fields (max {} for table display)\nUse --format json or --jq to process the full data",
obj.len(),
MAX_TABLE_ROWS
);
return Ok(output_or_capture(&msg, capture_output));
}
let rows: Vec<KeyValue> = obj
.iter()
.map(|(key, value)| KeyValue {
key: key.clone(),
value: format_value_for_table(value),
})
.collect();
let table = Table::new(&rows);
Ok(output_or_capture(&table.to_string(), capture_output))
}
_ => {
let formatted = format_value_for_table(json_value);
Ok(output_or_capture(&formatted, capture_output))
}
}
}
fn format_value_for_table(value: &Value) -> String {
match value {
Value::Null => constants::NULL_VALUE.to_string(),
Value::Bool(b) => b.to_string(),
Value::Number(n) => n.to_string(),
Value::String(s) => s.clone(),
Value::Array(arr) => {
if arr.len() <= 3 {
format!(
"[{}]",
arr.iter()
.map(format_value_for_table)
.collect::<Vec<_>>()
.join(", ")
)
} else {
format!("[{} items]", arr.len())
}
}
Value::Object(obj) => {
if obj.len() <= 2 {
format!(
"{{{}}}",
obj.iter()
.map(|(k, v)| format!("{}: {}", k, format_value_for_table(v)))
.collect::<Vec<_>>()
.join(", ")
)
} else {
format!("{{object with {} fields}}", obj.len())
}
}
}
}