zilliz 0.1.1

TUI and CLI tool for managing Zilliz Cloud clusters and Milvus operations
Documentation
use serde_json::Value;

/// Format a JSON value as pretty-printed JSON string.
pub fn format_json(value: &Value) -> String {
    serde_json::to_string_pretty(value).unwrap_or_else(|_| value.to_string())
}

/// Format a JSON value as YAML.
pub fn format_yaml(value: &Value) -> String {
    serde_yaml::to_string(value)
        .unwrap_or_else(|_| format_json(value))
        .trim_end()
        .to_string()
}

/// Format a JSON value as CSV.
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()
}

/// Apply a JMESPath query filter to a JSON value.
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))
}

/// Format a JSON value as plain text (key: value pairs for objects, one line per item for arrays).
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(),
    }
}

/// Format a JSON array of objects as a table.
pub fn format_table(values: &[&Value], columns: &[&str]) -> String {
    format_table_with_opts(values, columns, false)
}

/// Format a JSON array of objects as a table, with optional header suppression.
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()
}

/// Auto-detect column names from the first item in a list of JSON objects.
/// Filters out overly nested or large fields.
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| {
            // Skip deeply nested objects/arrays that don't render well in tables
            !matches!(first.get(k.as_str()), Some(Value::Object(_)) | Some(Value::Array(_)))
        })
        .cloned()
        .collect()
}

/// Format an API error (with code) for the given output format.
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),
    }
}

/// Format a non-API error (no code) for the given output format.
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());
    }
}