use clap::{ArgMatches, Args, Command, FromArgMatches};
use crate::typesafe::question::{FlagEvent, FlagKind};
#[derive(Args)]
pub struct ClassifyFlags {
#[arg(long, value_name = "FILE", requires = "out", conflicts_with_all = ["state", "state_json"])]
pub batch: Option<String>,
#[arg(long, value_name = "FILE", requires = "batch")]
pub out: Option<String>,
#[arg(long, requires = "batch", value_parser = clap::value_parser!(u8).range(1..=16))]
pub jobs: Option<u8>,
#[arg(long, requires = "batch")]
pub resume: bool,
#[arg(long)]
pub state: Option<String>,
#[arg(long)]
pub state_json: bool,
#[arg(long, value_name = "ID=QUESTION")]
pub noul: Vec<String>,
#[arg(long, value_name = "ID=QUESTION")]
pub choice: Vec<String>,
#[arg(long, value_name = "A,B,C")]
pub options: Vec<String>,
#[arg(long, value_name = "ID=QUESTION")]
pub score: Vec<String>,
#[arg(long, value_name = "L0,L1,...")]
pub levels: Vec<String>,
#[arg(long, value_name = "FILE")]
pub questions: Option<String>,
#[arg(long, default_value = crate::typesafe::classify::DEFAULT_MODEL)]
pub model: String,
#[arg(long, default_value_t = crate::typesafe::classify::DEFAULT_TIMEOUT_SECS)]
pub timeout: u64,
#[arg(long)]
pub allow_secret_like: bool,
}
pub struct ClassifyArgs {
pub flags: ClassifyFlags,
pub events: Vec<FlagEvent>,
}
impl FromArgMatches for ClassifyArgs {
fn from_arg_matches(matches: &ArgMatches) -> Result<Self, clap::Error> {
let flags = ClassifyFlags::from_arg_matches(matches)?;
let mut indexed = Vec::new();
for (id, kind) in [
("noul", FlagKind::Noul),
("choice", FlagKind::Choice),
("options", FlagKind::Options),
("score", FlagKind::Score),
("levels", FlagKind::Levels),
] {
if let (Some(values), Some(indices)) = (matches.get_many::<String>(id), matches.indices_of(id)) {
indexed.extend(indices.zip(values).map(|(index, value)| (index, kind, value.clone())));
}
}
indexed.sort_by_key(|(index, _, _)| *index);
let events = indexed.into_iter().map(|(_, kind, value)| FlagEvent { kind, value }).collect();
Ok(Self { flags, events })
}
fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> Result<(), clap::Error> {
*self = Self::from_arg_matches(matches)?;
Ok(())
}
}
impl Args for ClassifyArgs {
fn augment_args(cmd: Command) -> Command {
ClassifyFlags::augment_args(cmd)
}
fn augment_args_for_update(cmd: Command) -> Command {
ClassifyFlags::augment_args_for_update(cmd)
}
}
#[cfg(test)]
mod tests {
use clap::Parser;
use crate::cli::{Cli, Commands};
use crate::typesafe::question::{FlagKind, questions_from_flags};
fn parse(args: &[&str]) -> super::ClassifyArgs {
let argv = ["aid", "classify"].iter().chain(args).copied();
let Some(Commands::Classify(parsed)) = Cli::try_parse_from(argv).expect("parse").command else {
panic!("expected classify");
};
parsed
}
#[test]
fn defaults_read_stdin_with_pinned_model() {
let args = parse(&["--noul", "a=A?"]);
assert_eq!(args.flags.state, None);
assert_eq!(args.flags.model, "jev-1.13.0");
assert_eq!(args.flags.timeout, 20);
assert!(!args.flags.state_json && !args.flags.allow_secret_like);
}
#[test]
fn events_keep_argv_order_across_flag_kinds() {
let args = parse(&[
"--choice", "v=Verdict?", "--options", "SHIP,FIX", "--noul", "t=Tests?",
"--choice", "l=Lang?", "--options", "rust,go", "--score", "r=Risk?", "--levels", "lo,hi",
]);
let kinds: Vec<FlagKind> = args.events.iter().map(|event| event.kind).collect();
assert_eq!(kinds, [
FlagKind::Choice, FlagKind::Options, FlagKind::Noul, FlagKind::Choice,
FlagKind::Options, FlagKind::Score, FlagKind::Levels,
]);
let built = questions_from_flags(&args.events).expect("paired");
let lang = built.iter().find(|(id, _)| id == "l").expect("lang");
assert_eq!(lang.1["criteria"], serde_json::json!({ "rust": null, "go": null }));
}
#[test]
fn grouped_choices_do_not_pair_with_later_options() {
let args = parse(&["--choice", "a=A?", "--choice", "b=B?", "--options", "x,y", "--options", "p,q"]);
assert!(questions_from_flags(&args.events).is_err());
}
#[test]
fn batch_flags_require_paths_and_bound_concurrency() {
let args = parse(&["--batch", "in.jsonl", "--out", "out.jsonl", "--resume", "--jobs", "16", "--noul", "a=A?"]);
assert_eq!(args.flags.jobs, Some(16));
assert!(args.flags.resume);
assert_eq!(parse(&["--batch", "in", "--out", "out"]).flags.jobs, None);
for flags in [
vec!["--batch", "in"], vec!["--out", "out"], vec!["--resume"], vec!["--jobs", "4"],
vec!["--batch", "in", "--out", "out", "--jobs", "0"],
vec!["--batch", "in", "--out", "out", "--jobs", "17"],
vec!["--batch", "in", "--out", "out", "--state", "file"],
vec!["--batch", "in", "--out", "out", "--state-json"],
] {
assert!(Cli::try_parse_from(["aid", "classify"].into_iter().chain(flags)).is_err());
}
}
}