use serde_json::Value;
use std::io::IsTerminal;
pub(crate) const DESCRIPTION_TRUNCATE_LEN: usize = 80;
pub(crate) fn resolve_format_inner(explicit_format: Option<&str>, is_tty: bool) -> &'static str {
if let Some(fmt) = explicit_format {
return match fmt {
"json" => "json",
"table" => "table",
"csv" => "csv",
"yaml" => "yaml",
"jsonl" => "jsonl",
other => {
tracing::warn!("Unknown format '{}', defaulting to 'json'.", other);
"json"
}
};
}
if is_tty {
"table"
} else {
"json"
}
}
pub fn resolve_format(explicit_format: Option<&str>) -> &'static str {
let is_tty = std::io::stdout().is_terminal();
resolve_format_inner(explicit_format, is_tty)
}
pub(crate) fn truncate(text: &str, max_length: usize) -> String {
if text.len() <= max_length {
return text.to_string();
}
let cutoff = max_length.saturating_sub(3);
let mut end = cutoff;
while end > 0 && !text.is_char_boundary(end) {
end -= 1;
}
format!("{}...", &text[..end])
}
fn extract_str<'a>(v: &'a Value, keys: &[&str]) -> &'a str {
for key in keys {
if let Some(s) = v.get(key).and_then(|s| s.as_str()) {
return s;
}
}
""
}
fn extract_tags(v: &Value) -> Vec<String> {
v.get("tags")
.and_then(|t| t.as_array())
.map(|arr| {
arr.iter()
.filter_map(|s| s.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default()
}
fn csv_scalar_string(v: &Value) -> String {
match v {
Value::String(s) => s.clone(),
other => other.to_string(),
}
}
fn csv_field(s: &str) -> String {
if s.contains(',') || s.contains('"') || s.contains('\n') || s.contains('\r') {
let escaped = s.replace('"', "\"\"");
format!("\"{escaped}\"")
} else {
s.to_string()
}
}
pub fn format_module_list(modules: &[Value], format: &str, filter_tags: &[&str]) -> String {
use comfy_table::{ContentArrangement, Table};
match format {
"table" => {
if modules.is_empty() {
if !filter_tags.is_empty() {
return format!(
"No modules found matching tags: {}.",
filter_tags.join(", ")
);
}
return "No modules found.".to_string();
}
let mut table = Table::new();
table.set_content_arrangement(ContentArrangement::Dynamic);
table.set_header(vec!["ID", "Description", "Tags"]);
for m in modules {
let id = extract_str(m, &["module_id", "id", "canonical_id", "name"]);
let desc_raw = extract_str(m, &["description"]);
let desc = truncate(desc_raw, DESCRIPTION_TRUNCATE_LEN);
let tags = extract_tags(m).join(", ");
table.add_row(vec![id.to_string(), desc, tags]);
}
table.to_string()
}
"json" => {
let result: Vec<serde_json::Value> = modules
.iter()
.map(|m| {
let id = extract_str(m, &["module_id", "id", "canonical_id", "name"]);
let desc = extract_str(m, &["description"]);
let tags: Vec<serde_json::Value> = extract_tags(m)
.into_iter()
.map(serde_json::Value::String)
.collect();
serde_json::json!({
"id": id,
"description": desc,
"tags": tags,
})
})
.collect();
serde_json::to_string_pretty(&result).unwrap_or_else(|_| "[]".to_string())
}
unknown => {
tracing::warn!(
"Unknown format '{}' in format_module_list, using json.",
unknown
);
format_module_list(modules, "json", filter_tags)
}
}
}
fn render_panel(title: &str) -> String {
use comfy_table::Table;
let mut table = Table::new();
table.load_preset(comfy_table::presets::UTF8_FULL);
table.add_row(vec![title]);
table.to_string()
}
fn render_section(title: &str, content: &str) -> Option<String> {
if content.is_empty() {
return None;
}
Some(format!("\n{}:\n{}", title, content))
}
pub fn format_module_detail(module: &Value, format: &str) -> String {
let id = extract_str(module, &["module_id", "id", "canonical_id", "name"]);
let description = extract_str(module, &["description"]);
match format {
"table" => {
let mut parts: Vec<String> = Vec::new();
parts.push(render_panel(&format!("Module: {}", id)));
parts.push(format!("\nDescription:\n {}", description));
if let Some(input_schema) = module.get("input_schema").filter(|v| !v.is_null()) {
let content =
serde_json::to_string_pretty(input_schema).unwrap_or_else(|_| "{}".to_string());
if let Some(section) = render_section("Input Schema", &content) {
parts.push(section);
}
}
if let Some(output_schema) = module.get("output_schema").filter(|v| !v.is_null()) {
let content = serde_json::to_string_pretty(output_schema)
.unwrap_or_else(|_| "{}".to_string());
if let Some(section) = render_section("Output Schema", &content) {
parts.push(section);
}
}
if let Some(ann) = module.get("annotations").and_then(|v| v.as_object()) {
if !ann.is_empty() {
let content: String = ann
.iter()
.map(|(k, v)| {
let val = v.as_str().unwrap_or(&v.to_string()).to_string();
format!(" {}: {}", k, val)
})
.collect::<Vec<_>>()
.join("\n");
if let Some(section) = render_section("Annotations", &content) {
parts.push(section);
}
}
}
let x_fields: Vec<(String, String)> = module
.as_object()
.map(|obj| {
obj.iter()
.filter(|(k, _)| k.starts_with("x-") || k.starts_with("x_"))
.map(|(k, v)| {
let val = v.as_str().unwrap_or(&v.to_string()).to_string();
(k.clone(), val)
})
.collect()
})
.unwrap_or_default();
if !x_fields.is_empty() {
let content: String = x_fields
.iter()
.map(|(k, v)| format!(" {}: {}", k, v))
.collect::<Vec<_>>()
.join("\n");
if let Some(section) = render_section("Extension Metadata", &content) {
parts.push(section);
}
}
let tags = extract_tags(module);
if !tags.is_empty() {
if let Some(section) = render_section("Tags", &format!(" {}", tags.join(", "))) {
parts.push(section);
}
}
parts.join("\n")
}
"json" => {
let mut result = serde_json::Map::new();
result.insert("id".to_string(), serde_json::Value::String(id.to_string()));
result.insert(
"description".to_string(),
serde_json::Value::String(description.to_string()),
);
for key in &["input_schema", "output_schema"] {
if let Some(v) = module.get(*key).filter(|v| !v.is_null()) {
result.insert(key.to_string(), v.clone());
}
}
if let Some(ann) = module
.get("annotations")
.filter(|v| !v.is_null() && v.as_object().is_some_and(|o| !o.is_empty()))
{
result.insert("annotations".to_string(), ann.clone());
}
let tags = extract_tags(module);
if !tags.is_empty() {
result.insert(
"tags".to_string(),
serde_json::Value::Array(
tags.into_iter().map(serde_json::Value::String).collect(),
),
);
}
if let Some(obj) = module.as_object() {
for (k, v) in obj {
if k.starts_with("x-") || k.starts_with("x_") {
result.insert(k.clone(), v.clone());
}
}
}
serde_json::to_string_pretty(&serde_json::Value::Object(result))
.unwrap_or_else(|_| "{}".to_string())
}
unknown => {
tracing::warn!(
"Unknown format '{}' in format_module_detail, using json.",
unknown
);
format_module_detail(module, "json")
}
}
}
fn apply_field_selection(result: &Value, fields: &str) -> Value {
if let Some(obj) = result.as_object() {
let mut selected = serde_json::Map::new();
for field in fields.split(',') {
let field = field.trim();
if field.is_empty() {
continue;
}
let mut val: &Value = &Value::Object(obj.clone());
for part in field.split('.') {
if let Some(next) = val.get(part) {
val = next;
} else {
val = &Value::Null;
break;
}
}
selected.insert(field.to_string(), val.clone());
}
Value::Object(selected)
} else {
result.clone()
}
}
pub fn format_exec_result(result: &Value, format: &str, fields: Option<&str>) -> String {
use comfy_table::{ContentArrangement, Table};
let result = if let Some(f) = fields {
apply_field_selection(result, f)
} else {
result.clone()
};
match &result {
Value::Null => String::new(),
Value::String(s) => s.clone(),
Value::Object(_) if format == "csv" => {
let obj = result.as_object().unwrap();
let keys: Vec<&String> = obj.keys().collect();
let header = keys
.iter()
.map(|k| csv_field(k.as_str()))
.collect::<Vec<_>>()
.join(",");
let values = keys
.iter()
.map(|k| {
let v = obj.get(*k).unwrap();
csv_field(&csv_scalar_string(v))
})
.collect::<Vec<_>>()
.join(",");
format!("{header}\n{values}")
}
Value::Array(arr) if format == "csv" => {
if arr.is_empty() {
return String::new();
}
if let Some(first_obj) = arr[0].as_object() {
let keys: Vec<&String> = first_obj.keys().collect();
let header = keys
.iter()
.map(|k| csv_field(k.as_str()))
.collect::<Vec<_>>()
.join(",");
let mut rows = vec![header];
for item in arr {
if let Some(obj) = item.as_object() {
let row = keys
.iter()
.map(|k| {
let v = obj.get(*k).unwrap_or(&Value::Null);
csv_field(&csv_scalar_string(v))
})
.collect::<Vec<_>>()
.join(",");
rows.push(row);
}
}
rows.join("\n")
} else {
serde_json::to_string(&result).unwrap_or_default()
}
}
_ if format == "yaml" => serde_yaml::to_string(&result)
.map(|s| s.trim_end().to_string())
.unwrap_or_else(|_| {
serde_json::to_string_pretty(&result).unwrap_or_else(|_| "null".to_string())
}),
Value::Array(arr) if format == "jsonl" => arr
.iter()
.map(|item| serde_json::to_string(item).unwrap_or_default())
.collect::<Vec<_>>()
.join("\n"),
_ if format == "jsonl" => serde_json::to_string(&result).unwrap_or_default(),
Value::Object(_) if format == "table" => {
let obj = result.as_object().unwrap();
let mut table = Table::new();
table.set_content_arrangement(ContentArrangement::Dynamic);
table.set_header(vec!["Key", "Value"]);
for (k, v) in obj {
let val_str = match v {
Value::String(s) => s.clone(),
other => other.to_string(),
};
table.add_row(vec![k.clone(), val_str]);
}
table.to_string()
}
Value::Object(_) | Value::Array(_) => {
serde_json::to_string_pretty(&result).unwrap_or_else(|_| "null".to_string())
}
other => other.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_resolve_format_explicit_json_tty() {
assert_eq!(resolve_format_inner(Some("json"), true), "json");
}
#[test]
fn test_resolve_format_explicit_table_non_tty() {
assert_eq!(resolve_format_inner(Some("table"), false), "table");
}
#[test]
fn test_resolve_format_none_tty() {
assert_eq!(resolve_format_inner(None, true), "table");
}
#[test]
fn test_resolve_format_none_non_tty() {
assert_eq!(resolve_format_inner(None, false), "json");
}
#[test]
fn test_truncate_short_string() {
let s = "hello";
assert_eq!(truncate(s, 80), "hello");
}
#[test]
fn test_truncate_exact_length() {
let s = "a".repeat(80);
assert_eq!(truncate(&s, 80), s);
}
#[test]
fn test_truncate_over_limit() {
let s = "a".repeat(100);
let result = truncate(&s, 80);
assert_eq!(result.len(), 80);
assert!(result.ends_with("..."));
assert_eq!(&result[..77], &"a".repeat(77));
}
#[test]
fn test_truncate_exactly_81_chars() {
let s = "b".repeat(81);
let result = truncate(&s, 80);
assert_eq!(result.len(), 80);
assert!(result.ends_with("..."));
}
#[test]
fn test_format_module_list_json_two_modules() {
let modules = vec![
json!({"module_id": "math.add", "description": "Add numbers", "tags": ["math"]}),
json!({"module_id": "text.upper", "description": "Uppercase", "tags": []}),
];
let output = format_module_list(&modules, "json", &[]);
let parsed: serde_json::Value = serde_json::from_str(&output).expect("must be valid JSON");
let arr = parsed.as_array().expect("must be array");
assert_eq!(arr.len(), 2);
assert_eq!(arr[0]["id"], "math.add");
assert_eq!(arr[1]["id"], "text.upper");
}
#[test]
fn test_format_module_list_json_empty() {
let output = format_module_list(&[], "json", &[]);
assert_eq!(output.trim(), "[]");
}
#[test]
fn test_format_module_list_table_two_modules() {
let modules =
vec![json!({"module_id": "math.add", "description": "Add numbers", "tags": ["math"]})];
let output = format_module_list(&modules, "table", &[]);
assert!(output.contains("math.add"), "table must contain module ID");
assert!(
output.contains("Add numbers"),
"table must contain description"
);
}
#[test]
fn test_format_module_list_table_columns() {
let modules =
vec![json!({"module_id": "math.add", "description": "Add numbers", "tags": []})];
let output = format_module_list(&modules, "table", &[]);
assert!(output.contains("ID"), "table must have ID column");
assert!(
output.contains("Description"),
"table must have Description column"
);
assert!(output.contains("Tags"), "table must have Tags column");
}
#[test]
fn test_format_module_list_table_empty_no_tags() {
let output = format_module_list(&[], "table", &[]);
assert_eq!(output.trim(), "No modules found.");
}
#[test]
fn test_format_module_list_table_empty_with_filter_tags() {
let output = format_module_list(&[], "table", &["math", "text"]);
assert!(
output.contains("No modules found matching tags:"),
"must contain tag-filter message"
);
assert!(output.contains("math"), "must contain tag name");
assert!(output.contains("text"), "must contain tag name");
}
#[test]
fn test_format_module_list_table_description_truncated() {
let long_desc = "a".repeat(100);
let modules = vec![json!({"module_id": "x.y", "description": long_desc, "tags": []})];
let output = format_module_list(&modules, "table", &[]);
assert!(
output.contains("..."),
"long description must be truncated with '...'"
);
assert!(
!output.contains(&"a".repeat(100)),
"full description must not appear"
);
}
#[test]
fn test_format_module_list_json_tags_present() {
let modules = vec![json!({"module_id": "a.b", "description": "desc", "tags": ["x", "y"]})];
let output = format_module_list(&modules, "json", &[]);
let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
let tags = parsed[0]["tags"].as_array().unwrap();
assert_eq!(tags.len(), 2);
assert_eq!(tags[0], "x");
}
#[test]
fn test_format_exec_result_null_returns_empty() {
let output = format_exec_result(&Value::Null, "json", None);
assert_eq!(output, "", "Null result must produce empty string");
}
#[test]
fn test_format_exec_result_string_plain() {
let result = json!("hello world");
let output = format_exec_result(&result, "json", None);
assert_eq!(output, "hello world");
}
#[test]
fn test_format_exec_result_string_table_mode_also_plain() {
let result = json!("hello");
let output = format_exec_result(&result, "table", None);
assert_eq!(output, "hello");
}
#[test]
fn test_format_exec_result_object_json_mode() {
let result = json!({"sum": 42, "status": "ok"});
let output = format_exec_result(&result, "json", None);
let parsed: serde_json::Value = serde_json::from_str(&output).expect("must be valid JSON");
assert_eq!(parsed["sum"], 42);
assert_eq!(parsed["status"], "ok");
}
#[test]
fn test_format_exec_result_object_table_mode() {
let result = json!({"key": "value", "count": 3});
let output = format_exec_result(&result, "table", None);
assert!(output.contains("key"), "table must contain 'key'");
assert!(output.contains("value"), "table must contain 'value'");
assert!(output.contains("count"), "table must contain 'count'");
assert!(output.contains('3'), "table must contain '3'");
}
#[test]
fn test_format_exec_result_array_is_json() {
let result = json!([1, 2, 3]);
let output = format_exec_result(&result, "json", None);
let parsed: serde_json::Value = serde_json::from_str(&output).expect("must be valid JSON");
assert!(parsed.is_array());
assert_eq!(parsed.as_array().unwrap().len(), 3);
}
#[test]
fn test_format_exec_result_array_table_mode_is_json() {
let result = json!([{"a": 1}, {"b": 2}]);
let output = format_exec_result(&result, "table", None);
let parsed: serde_json::Value =
serde_json::from_str(&output).expect("array must produce JSON");
assert!(parsed.is_array());
}
#[test]
fn test_format_exec_result_number_scalar() {
let result = json!(42);
let output = format_exec_result(&result, "json", None);
assert_eq!(output, "42");
}
#[test]
fn test_format_exec_result_bool_scalar() {
let result = json!(true);
let output = format_exec_result(&result, "json", None);
assert_eq!(output, "true");
}
#[test]
fn test_format_exec_result_float_scalar() {
let result = json!(3.15);
let output = format_exec_result(&result, "json", None);
assert!(output.starts_with("3.15"), "float must stringify correctly");
}
#[test]
fn test_format_module_detail_json_full() {
let module = json!({
"module_id": "math.add",
"description": "Add two numbers",
"input_schema": {"type": "object", "properties": {"a": {"type": "integer"}}},
"output_schema": {"type": "object", "properties": {"result": {"type": "integer"}}},
"tags": ["math"],
"annotations": {"author": "test"}
});
let output = format_module_detail(&module, "json");
let parsed: serde_json::Value = serde_json::from_str(&output).expect("must be valid JSON");
assert_eq!(parsed["id"], "math.add");
assert_eq!(parsed["description"], "Add two numbers");
assert!(
parsed.get("input_schema").is_some(),
"input_schema must be present"
);
assert!(
parsed.get("output_schema").is_some(),
"output_schema must be present"
);
let tags = parsed["tags"].as_array().unwrap();
assert_eq!(tags[0], "math");
}
#[test]
fn test_format_module_detail_json_no_output_schema() {
let module = json!({
"module_id": "text.upper",
"description": "Uppercase",
});
let output = format_module_detail(&module, "json");
let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
assert!(
parsed.get("output_schema").is_none(),
"output_schema must be absent when not set"
);
}
#[test]
fn test_format_module_detail_json_no_none_fields() {
let module = json!({
"module_id": "a.b",
"description": "desc",
"input_schema": null,
"output_schema": null,
"tags": null,
});
let output = format_module_detail(&module, "json");
let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
assert!(
parsed.get("input_schema").is_none(),
"null input_schema must be absent"
);
assert!(parsed.get("tags").is_none(), "null tags must be absent");
}
#[test]
fn test_format_module_detail_table_contains_description() {
let module = json!({
"module_id": "math.add",
"description": "Add two numbers",
});
let output = format_module_detail(&module, "table");
assert!(
output.contains("Add two numbers"),
"table must contain description"
);
}
#[test]
fn test_format_module_detail_table_contains_module_id() {
let module = json!({
"module_id": "math.add",
"description": "desc",
});
let output = format_module_detail(&module, "table");
assert!(output.contains("math.add"), "table must contain module ID");
}
#[test]
fn test_format_module_detail_table_input_schema_section() {
let module = json!({
"module_id": "math.add",
"description": "desc",
"input_schema": {"type": "object"}
});
let output = format_module_detail(&module, "table");
assert!(
output.contains("Input Schema"),
"table must contain Input Schema section"
);
}
#[test]
fn test_format_module_detail_table_no_output_schema_section_when_absent() {
let module = json!({
"module_id": "text.upper",
"description": "desc",
});
let output = format_module_detail(&module, "table");
assert!(
!output.contains("Output Schema"),
"Output Schema section must be absent when not set"
);
}
#[test]
fn test_format_module_detail_table_tags_section() {
let module = json!({
"module_id": "math.add",
"description": "desc",
"tags": ["math", "arithmetic"]
});
let output = format_module_detail(&module, "table");
assert!(output.contains("Tags"), "table must contain Tags section");
assert!(output.contains("math"), "table must contain tag value");
}
#[test]
fn test_format_module_detail_table_annotations_section() {
let module = json!({
"module_id": "a.b",
"description": "desc",
"annotations": {"author": "alice", "version": "1.0"}
});
let output = format_module_detail(&module, "table");
assert!(
output.contains("Annotations"),
"table must contain Annotations section"
);
assert!(
output.contains("author"),
"table must contain annotation key"
);
assert!(
output.contains("alice"),
"table must contain annotation value"
);
}
#[test]
fn test_format_module_detail_table_extension_metadata() {
let module = json!({
"module_id": "a.b",
"description": "desc",
"x-category": "utility"
});
let output = format_module_detail(&module, "table");
assert!(
output.contains("Extension Metadata"),
"must contain Extension Metadata section"
);
assert!(output.contains("x-category"), "must contain x- key");
assert!(output.contains("utility"), "must contain x- value");
}
}