use serde_json::Value;
pub fn format_json(value: &Value) -> String {
serde_json::to_string_pretty(value).unwrap_or_else(|_| value.to_string())
}
pub fn format_yaml(value: &Value) -> String {
serde_yaml::to_string(value)
.unwrap_or_else(|_| format_json(value))
.trim_end()
.to_string()
}
pub fn format_csv(value: &Value, no_header: bool) -> String {
let items = match value {
Value::Array(arr) => arr.iter().collect::<Vec<_>>(),
Value::Object(_) => vec![value],
other => return other.to_string(),
};
if items.is_empty() {
return String::new();
}
let mut wtr = csv::WriterBuilder::new().from_writer(vec![]);
if let Some(first) = items.first().and_then(|v| v.as_object()) {
let keys: Vec<&String> = first.keys().collect();
if !no_header {
let headers: Vec<&str> = keys.iter().map(|k| k.as_str()).collect();
let _ = wtr.write_record(&headers);
}
for item in &items {
let row: Vec<String> = keys
.iter()
.map(|k| {
item.get(k.as_str())
.map(|v| match v {
Value::String(s) => s.clone(),
Value::Null => String::new(),
other => other.to_string(),
})
.unwrap_or_default()
})
.collect();
let _ = wtr.write_record(&row);
}
} else {
if !no_header {
let _ = wtr.write_record(["value"]);
}
for item in &items {
let val = match item {
Value::String(s) => s.clone(),
other => other.to_string(),
};
let _ = wtr.write_record([&val]);
}
}
let _ = wtr.flush();
String::from_utf8(wtr.into_inner().unwrap_or_default())
.unwrap_or_default()
.trim_end_matches('\n')
.to_string()
}
pub fn apply_query(value: &Value, expression: &str) -> anyhow::Result<Value> {
let expr = jmespath::compile(expression)
.map_err(|e| anyhow::anyhow!("Invalid JMESPath expression: {}", e))?;
let jmes_data = jmespath::Variable::from_json(&value.to_string())
.map_err(|e| anyhow::anyhow!("Failed to parse data for JMESPath: {}", e))?;
let result = expr.search(jmes_data)
.map_err(|e| anyhow::anyhow!("JMESPath search failed: {}", e))?;
let json_str = serde_json::to_string(&*result)
.map_err(|e| anyhow::anyhow!("Failed to serialize JMESPath result: {}", e))?;
serde_json::from_str(&json_str)
.map_err(|e| anyhow::anyhow!("Failed to parse JMESPath result: {}", e))
}
pub fn format_text(value: &Value) -> String {
match value {
Value::Object(map) => map
.iter()
.map(|(k, v)| {
let val = match v {
Value::String(s) => s.clone(),
Value::Null => "".to_string(),
other => other.to_string(),
};
format!("{}: {}", k, val)
})
.collect::<Vec<_>>()
.join("\n"),
Value::Array(arr) => arr
.iter()
.map(format_text)
.collect::<Vec<_>>()
.join("\n---\n"),
Value::String(s) => s.clone(),
Value::Null => "".to_string(),
other => other.to_string(),
}
}
pub fn format_table(values: &[&Value], columns: &[&str]) -> String {
format_table_with_opts(values, columns, false)
}
pub fn format_table_with_opts(values: &[&Value], columns: &[&str], no_header: bool) -> String {
use comfy_table::{presets::UTF8_FULL_CONDENSED, Table};
let mut table = Table::new();
table.load_preset(UTF8_FULL_CONDENSED);
if !no_header {
table.set_header(columns);
}
for item in values {
let row: Vec<String> = columns
.iter()
.map(|col| {
item.get(col)
.map(|v| match v {
Value::String(s) => s.clone(),
Value::Null => "".to_string(),
other => other.to_string(),
})
.unwrap_or_default()
})
.collect();
table.add_row(row);
}
table.to_string()
}
pub fn auto_columns(items: &[&Value]) -> Vec<String> {
let first = match items.first() {
Some(item) => item,
None => return vec![],
};
let obj = match first.as_object() {
Some(o) => o,
None => return vec![],
};
obj.keys()
.filter(|k| {
!matches!(first.get(k.as_str()), Some(Value::Object(_)) | Some(Value::Array(_)))
})
.cloned()
.collect()
}
pub fn format_error(format: &str, code: i64, message: &str) -> String {
match format {
"json" => {
let obj = serde_json::json!({"code": code, "message": message});
serde_json::to_string_pretty(&obj).unwrap_or_else(|_| obj.to_string())
}
_ => format!("Error [{}]: {}", code, message),
}
}
pub fn format_error_simple(format: &str, message: &str) -> String {
match format {
"json" => {
let obj = serde_json::json!({"code": 0, "message": message});
serde_json::to_string_pretty(&obj).unwrap_or_else(|_| obj.to_string())
}
_ => format!("Error: {}", message),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_format_yaml() {
let data = json!({"name": "test", "count": 42});
let yaml = format_yaml(&data);
assert!(yaml.contains("name: test"));
assert!(yaml.contains("count: 42"));
}
#[test]
fn test_format_csv_list() {
let data = json!([
{"id": "1", "name": "alice"},
{"id": "2", "name": "bob"}
]);
let csv = format_csv(&data, false);
assert!(csv.contains("id,name"));
assert!(csv.contains("1,alice"));
assert!(csv.contains("2,bob"));
}
#[test]
fn test_format_csv_no_header() {
let data = json!([{"id": "1", "name": "alice"}]);
let csv = format_csv(&data, true);
assert!(!csv.contains("id,name"));
assert!(csv.contains("1,alice"));
}
#[test]
fn test_format_csv_single_object() {
let data = json!({"id": "1", "name": "alice"});
let csv = format_csv(&data, false);
assert!(csv.contains("id,name"));
assert!(csv.contains("1,alice"));
}
#[test]
fn test_apply_query() {
let data = json!({"items": [1, 2, 3], "count": 3});
let result = apply_query(&data, "items").unwrap();
assert_eq!(result, json!([1, 2, 3]));
}
#[test]
fn test_apply_query_invalid() {
let data = json!({"a": 1});
assert!(apply_query(&data, "invalid[[[").is_err());
}
}