use std::collections::HashMap;
use clap::{Parser, Subcommand};
use clap_complete::Shell;
#[derive(Parser, Debug)]
#[command(name = "seedgen")]
#[command(about = "Zero-config database seed data generator", long_about = None)]
#[command(version)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
#[arg(long, short = 'u', env = "DATABASE_URL", global = true)]
pub url: Option<String>,
#[arg(long, short, action = clap::ArgAction::Count, global = true)]
pub verbose: u8,
#[arg(long, short, global = true)]
pub quiet: bool,
#[arg(long, global = true)]
pub no_color: bool,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
Introspect {
#[arg(long, default_value = "table")]
format: String,
#[arg(long)]
output: Option<String>,
#[arg(long, value_delimiter = ',')]
include: Option<Vec<String>>,
#[arg(long, value_delimiter = ',')]
exclude: Option<Vec<String>>,
},
Generate {
#[arg(long)]
seed: Option<u64>,
#[arg(long, default_value = "10")]
rows: usize,
#[arg(long)]
scenario: Option<String>,
#[arg(long, short = 'f')]
file: Option<String>,
#[arg(long, value_parser = parse_entities)]
entities: Option<HashMap<String, usize>>,
#[arg(long)]
output: Option<String>,
#[arg(long, default_value = "sql")]
format: String,
#[arg(long)]
fast: bool,
#[arg(long)]
dry_run: bool,
#[arg(long, default_value = "en")]
locale: String,
#[arg(long, value_delimiter = ',')]
include: Option<Vec<String>>,
#[arg(long, value_delimiter = ',')]
exclude: Option<Vec<String>>,
#[arg(long)]
truncate_first: bool,
},
Reset {
#[arg(long)]
confirm: bool,
#[arg(long, value_delimiter = ',')]
only: Option<Vec<String>>,
#[arg(long, default_value = "true")]
cascade: bool,
},
Validate {
#[arg(long, short = 'f')]
file: String,
},
McpServer {
#[arg(long, default_value = "stdio")]
transport: String,
#[arg(long, default_value = "3100")]
port: u16,
},
Completions {
shell: Shell,
},
}
fn parse_entities(s: &str) -> Result<HashMap<String, usize>, String> {
let mut map = HashMap::new();
for pair in s.split(',') {
let (k, v) = pair
.split_once('=')
.ok_or_else(|| format!("expected `name=count`, got `{pair}`"))?;
let k = k.trim();
let count: usize = v
.trim()
.parse()
.map_err(|e| format!("invalid count for `{k}`: {e}"))?;
if k.is_empty() {
return Err("empty table name".into());
}
map.insert(k.to_string(), count);
}
Ok(map)
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn test_cli_builds_without_panic() {
Cli::command().debug_assert();
}
#[test]
fn test_parse_entities_simple() {
let m = parse_entities("users=100,orders=500").unwrap();
assert_eq!(m.get("users"), Some(&100));
assert_eq!(m.get("orders"), Some(&500));
}
#[test]
fn test_parse_entities_with_whitespace() {
let m = parse_entities("users = 50, products = 200").unwrap();
assert_eq!(m.get("users"), Some(&50));
assert_eq!(m.get("products"), Some(&200));
}
#[test]
fn test_parse_entities_missing_equals_errors() {
assert!(parse_entities("users 100").is_err());
}
#[test]
fn test_parse_entities_invalid_count_errors() {
assert!(parse_entities("users=abc").is_err());
}
#[test]
fn test_parse_entities_empty_name_errors() {
assert!(parse_entities("=100").is_err());
}
}