zilliz 0.1.1

TUI and CLI tool for managing Zilliz Cloud clusters and Milvus operations
Documentation
use crate::cli::args::Commands;
use crate::model::loader::Models;
use crate::model::types::{Operation, Resource};

const CLOUD_MANAGEMENT: &[&str] = &[
    "cluster", "project", "backup", "import", "volume", "job", "billing", "alert",
];

const DATA_OPERATIONS: &[&str] = &[
    "collection", "vector", "database", "index", "partition", "user", "role", "alias",
];

struct ConfigCmd {
    name: &'static str,
    description: &'static str,
}

const CONFIGURATION: &[ConfigCmd] = &[
    ConfigCmd {
        name: "configure",
        description: "Configure API key and default settings.",
    },
    ConfigCmd {
        name: "context",
        description: "Manage current cluster context.",
    },
    ConfigCmd {
        name: "auth",
        description: "Authentication commands.",
    },
    ConfigCmd {
        name: "login",
        description: "Log in to Zilliz Cloud.",
    },
    ConfigCmd {
        name: "logout",
        description: "Log out and clear stored credentials.",
    },
    ConfigCmd {
        name: "completion",
        description: "Shell completion management.",
    },
    ConfigCmd {
        name: "version",
        description: "Show CLI version.",
    },
];

/// Render the full grouped help text.
pub fn render_help(models: &Models) -> String {
    let version = env!("CARGO_PKG_VERSION");
    let mut out = String::new();

    out.push_str(&format!("CLI and TUI for Zilliz Cloud {}\n\n", version));
    out.push_str("Usage: zz [OPTIONS] <COMMAND> [ARGS]\n\n");

    // Cloud Management
    out.push_str("Cloud Management:\n");
    append_resource_group(&mut out, CLOUD_MANAGEMENT, models);
    out.push('\n');

    // Data Operations
    out.push_str("Data Operations:\n");
    append_resource_group(&mut out, DATA_OPERATIONS, models);
    out.push('\n');

    // Configuration
    out.push_str("Configuration:\n");
    for cmd in CONFIGURATION {
        out.push_str(&format!("  {:16}{}\n", cmd.name, cmd.description));
    }
    out.push('\n');

    // Global options
    out.push_str(
        "Options:\n\
         \x20     --api-key <KEY>       API key (overrides env/config) [env: ZILLIZ_API_KEY]\n\
         \x20 -o, --output <FORMAT>     Output format: json, table, text, yaml, csv [default: table]\n\
         \x20 -a, --all                 Fetch all pages for paginated results\n\
         \x20     --query <QUERY>       JMESPath query to filter output\n\
         \x20     --no-header           Suppress table/CSV header row\n\
         \x20     --wait                Wait for async jobs to complete\n\
         \x20 -h, --help                Print help\n\
         \x20 -V, --version             Print version\n",
    );

    out
}

fn append_resource_group(out: &mut String, names: &[&str], models: &Models) {
    for &name in names {
        let desc = lookup_description(name, models);
        out.push_str(&format!("  {:16}{}\n", name, desc));
    }
}

/// Return the clap subcommand name for a parsed `Commands` variant.
pub fn subcommand_name(cmd: &Commands) -> &'static str {
    match cmd {
        Commands::Configure { .. } => "configure",
        Commands::Context(_) => "context",
        Commands::Version => "version",
        Commands::Login { .. } => "login",
        Commands::Logout => "logout",
        Commands::Auth(_) => "auth",
        Commands::Completion(_) => "completion",
        Commands::Alert { .. } => "alert",
        Commands::External(_) => "external",
    }
}

fn lookup_description<'a>(name: &str, models: &'a Models) -> &'a str {
    if let Some(r) = models.control_plane.resources.get(name) {
        if let Some(ref d) = r.description {
            return d.as_str();
        }
    }
    if let Some(r) = models.data_plane.resources.get(name) {
        if let Some(ref d) = r.description {
            return d.as_str();
        }
    }
    ""
}

/// Extra operations that are hand-written (not in JSON models) per resource.
const HAND_WRITTEN_OPS: &[(&str, &str, &str)] = &[
    ("cluster", "create", "Create a new cluster."),
    ("cluster", "metrics", "Show cluster metrics."),
    ("billing", "usage", "Show billing usage summary."),
    ("billing", "invoices", "List invoices."),
];

/// Find a resource across both model files.
pub fn find_resource<'a>(models: &'a Models, name: &str) -> Option<&'a Resource> {
    models
        .control_plane
        .resources
        .get(name)
        .or_else(|| models.data_plane.resources.get(name))
}

/// Render help for a resource: description + operations list.
pub fn render_resource_help(resource_name: &str, resource: &Resource) -> String {
    let mut out = String::new();
    let desc = resource
        .description
        .as_deref()
        .unwrap_or("No description.");
    out.push_str(&format!("{}\n\n", desc));
    out.push_str(&format!(
        "Usage: zz {} <OPERATION> [OPTIONS]\n\n",
        resource_name
    ));
    out.push_str("Operations:\n");

    // Hand-written ops for this resource (listed first if not already in model)
    for &(res, op, desc) in HAND_WRITTEN_OPS {
        if res == resource_name && !resource.operations.contains_key(op) {
            out.push_str(&format!("  {:24}{}\n", op, desc));
        }
    }

    for (op_name, op) in &resource.operations {
        let op_desc = op.description.as_deref().unwrap_or("");
        out.push_str(&format!("  {:24}{}\n", op_name, op_desc));
    }

    out
}

/// Render help for a specific operation: description, flags, examples.
pub fn render_operation_help(
    resource_name: &str,
    op_name: &str,
    operation: &Operation,
) -> String {
    let mut out = String::new();
    let desc = operation
        .description
        .as_deref()
        .unwrap_or("No description.");
    out.push_str(&format!("{}\n\n", desc));
    out.push_str(&format!(
        "Usage: zz {} {} [OPTIONS]\n\n",
        resource_name, op_name
    ));

    if !operation.params.is_empty() {
        out.push_str("Options:\n");
        for p in &operation.params {
            let flag = p.cli_flag();
            let mut meta = format!("<{}>", p.param_type);
            if p.required {
                meta.push_str(" (required)");
            }
            if let Some(ref def) = p.default {
                meta.push_str(&format!(" [default: {}]", def));
            }
            if let Some(ref choices) = p.choices {
                meta.push_str(&format!(" [{}]", choices.join(", ")));
            }
            let desc = p.description.as_deref().unwrap_or("");
            out.push_str(&format!("  {:24}{:30}{}\n", flag, meta, desc));
        }
        out.push('\n');
    }

    if !operation.examples.is_empty() {
        out.push_str("Examples:\n");
        for ex in &operation.examples {
            if ex.is_empty() {
                out.push('\n');
            } else {
                out.push_str(&format!("  {}\n", ex));
            }
        }
    }

    out
}

/// List all available resource names.
pub fn available_resources(models: &Models) -> Vec<&str> {
    let mut names: Vec<&str> = Vec::new();
    for name in models.control_plane.resources.keys() {
        names.push(name.as_str());
    }
    for name in models.data_plane.resources.keys() {
        names.push(name.as_str());
    }
    names
}