clever-project 0.0.2

Declare Clever Cloud resources in a YAML/JSON file and sync them via the clever-tools CLI.
use std::path::PathBuf;

use clap::{Args, Parser, Subcommand};

#[derive(Debug, Parser)]
#[command(name = "clever-project", version, about = "Sync a project description with Clever Cloud", long_about = None)]
pub struct Cli {
    /// Verbose output
    #[arg(short, long, global = true)]
    pub verbose: bool,

    #[command(subcommand)]
    pub command: Command,
}

#[derive(Debug, Subcommand)]
pub enum Command {
    /// Read resources from a Clever Cloud org into a project file
    Read(ReadArgs),
    /// Create or update resources from a project file
    Apply(ApplyArgs),
    /// Delete resources listed in a project file
    Delete(DeleteArgs),
    /// Validate a project file (syntax, variables, dependencies, sizing,
    /// kinds, regions). Doesn't modify anything.
    Check(CheckArgs),
}

#[derive(Debug, Args)]
pub struct ReadArgs {
    /// Target organisation (overrides project file)
    #[arg(long)]
    pub org: String,

    /// App name to read (can be repeated)
    #[arg(long = "app")]
    pub apps: Vec<String>,

    /// Addon name to read (can be repeated)
    #[arg(long = "addon")]
    pub addons: Vec<String>,

    /// Read every app and addon in the org
    #[arg(long, conflicts_with_all = ["apps", "addons"])]
    pub all: bool,

    /// Output file path (.yaml/.yml/.json)
    #[arg(short = 'o', long)]
    pub output: PathBuf,
}

#[derive(Debug, Args)]
pub struct ApplyArgs {
    /// Project file path (.yaml/.yml/.json)
    pub file: PathBuf,

    /// Override the organisation defined in the project file
    #[arg(long)]
    pub org: Option<String>,

    /// Override the default region defined in the project file
    #[arg(long)]
    pub region: Option<String>,

    /// Value for the special variable `${env}` (default `prod`)
    #[arg(long)]
    pub env: Option<String>,

    /// Set a variable (key=value). Overrides values from the project file
    /// and from --variable-path.
    #[arg(long = "variable", value_parser = parse_kv)]
    pub variables: Vec<(String, String)>,

    /// Load variable overrides from a YAML/JSON file (flat key/value
    /// mapping). Can be repeated; later files override earlier ones, and
    /// --variable beats anything from these files.
    #[arg(long = "variable-path")]
    pub variable_paths: Vec<PathBuf>,

    /// Explicit path to a secrets file. When omitted, secrets are
    /// auto-discovered next to the project file (`<stem>.secrets` and
    /// `<stem>.<env>.secrets`).
    #[arg(long)]
    pub secrets_path: Option<PathBuf>,

    /// Plan only: read current state and log what would change without
    /// mutating anything on Clever Cloud.
    #[arg(long)]
    pub dry_run: bool,
}

#[derive(Debug, Args)]
pub struct CheckArgs {
    /// Project file path (.yaml/.yml/.json)
    pub file: PathBuf,

    /// Override the organisation defined in the project file
    #[arg(long)]
    pub org: Option<String>,

    /// Override the default region defined in the project file
    #[arg(long)]
    pub region: Option<String>,

    /// Value for the special variable `${env}` (default `prod`)
    #[arg(long)]
    pub env: Option<String>,

    /// Set a variable (key=value). Overrides values from the project file
    /// and from --variable-path.
    #[arg(long = "variable", value_parser = parse_kv)]
    pub variables: Vec<(String, String)>,

    /// Load variable overrides from a YAML/JSON file (repeatable).
    #[arg(long = "variable-path")]
    pub variable_paths: Vec<PathBuf>,

    /// Explicit path to a secrets file.
    #[arg(long)]
    pub secrets_path: Option<PathBuf>,

    /// Skip live validation against Clever's API (addon catalog, app
    /// instance flavors). Useful in CI environments without `clever login`.
    /// Static validation (syntax, variables, kinds, regions, dependencies,
    /// uniqueness) is always performed.
    #[arg(long)]
    pub offline: bool,
}

#[derive(Debug, Args)]
pub struct DeleteArgs {
    /// Project file path (.yaml/.yml/.json)
    pub file: PathBuf,

    /// Override the organisation defined in the project file
    #[arg(long)]
    pub org: Option<String>,

    /// Override the default region defined in the project file
    #[arg(long)]
    pub region: Option<String>,

    /// Value for the special variable `${env}` (default `prod`)
    #[arg(long)]
    pub env: Option<String>,

    /// Set a variable (key=value). Overrides values from the project file
    /// and from --variable-path.
    #[arg(long = "variable", value_parser = parse_kv)]
    pub variables: Vec<(String, String)>,

    /// Load variable overrides from a YAML/JSON file (flat key/value
    /// mapping). Can be repeated; later files override earlier ones, and
    /// --variable beats anything from these files.
    #[arg(long = "variable-path")]
    pub variable_paths: Vec<PathBuf>,

    /// Explicit path to a secrets file. When omitted, secrets are
    /// auto-discovered next to the project file (`<stem>.secrets` and
    /// `<stem>.<env>.secrets`).
    #[arg(long)]
    pub secrets_path: Option<PathBuf>,

    /// Plan only: log what would be deleted without mutating anything.
    #[arg(long)]
    pub dry_run: bool,
}

fn parse_kv(s: &str) -> Result<(String, String), String> {
    let (k, v) = s
        .split_once('=')
        .ok_or_else(|| format!("expected key=value, got `{s}`"))?;
    if k.is_empty() {
        return Err(format!("empty key in `{s}`"));
    }
    Ok((k.to_string(), v.to_string()))
}