use std::path::PathBuf;
use super::Command;
pub(super) fn parse_command_args(args: &[String]) -> Result<Command, String> {
match args[1].as_str() {
"help" | "-h" | "--help" => Ok(Command::Help),
"put-edge" => parse_put(args, |node_id, version, payload| Command::PutEdge {
node_id,
version,
payload,
}),
"put-vector" => parse_put(args, |node_id, version, payload| Command::PutVector {
node_id,
version,
payload,
}),
"put-full" => {
if args.len() < 5 {
return Err("usage: ir put-full <node_id> <version> <adjacency_csv>".to_string());
}
let node_id = args[2]
.parse::<u64>()
.map_err(|_| "invalid node_id".to_string())?;
let version = args[3]
.parse::<u64>()
.map_err(|_| "invalid version".to_string())?;
let adjacency = parse_csv_u64(&args[4])?;
Ok(Command::PutFull {
node_id,
version,
adjacency,
})
}
"get" => {
if args.len() < 3 {
return Err("usage: ir get <node_id>".to_string());
}
let node_id = args[2]
.parse::<u64>()
.map_err(|_| "invalid node_id".to_string())?;
Ok(Command::Get { node_id })
}
"traverse" => {
if args.len() < 3 {
return Err("usage: ir traverse <node_id>".to_string());
}
let node_id = args[2]
.parse::<u64>()
.map_err(|_| "invalid node_id".to_string())?;
Ok(Command::Traverse { node_id })
}
"recover" => Ok(Command::Recover),
"collect-stats" => Ok(Command::CollectStats),
"stats" => Ok(Command::Stats),
"health" => Ok(Command::Health),
"metrics" => Ok(Command::Metrics),
"metrics-prom" => Ok(Command::MetricsProm),
"status" => Ok(Command::Status),
"trigger-compaction" => {
let mut level: Option<u32> = None;
let mut idx = 2;
while idx < args.len() {
match args[idx].as_str() {
"--level" => {
if idx + 1 >= args.len() {
return Err("missing value for --level".to_string());
}
level = Some(
args[idx + 1]
.parse::<u32>()
.map_err(|_| "invalid level".to_string())?,
);
idx += 2;
}
other => {
return Err(format!("unknown trigger-compaction option: {}", other));
}
}
}
Ok(Command::TriggerCompaction { level })
}
"recluster" => {
let mut force = false;
let mut idx = 2;
while idx < args.len() {
match args[idx].as_str() {
"--force" => {
force = true;
idx += 1;
}
other => {
return Err(format!("unknown recluster option: {}", other));
}
}
}
Ok(Command::Recluster { force })
}
"ingest-edge" => {
if args.len() < 5 {
return Err("usage: ir ingest-edge <node_id> <version> <payload>".to_string());
}
let node_id = args[2]
.parse::<u64>()
.map_err(|_| "invalid node_id".to_string())?;
let version = args[3]
.parse::<u64>()
.map_err(|_| "invalid version".to_string())?;
Ok(Command::IngestEdge {
node_id,
version,
payload: args[4].as_bytes().to_vec(),
})
}
"ingest-vector" => {
if args.len() < 5 {
return Err("usage: ir ingest-vector <node_id> <version> <payload>".to_string());
}
let node_id = args[2]
.parse::<u64>()
.map_err(|_| "invalid node_id".to_string())?;
let version = args[3]
.parse::<u64>()
.map_err(|_| "invalid version".to_string())?;
Ok(Command::IngestVector {
node_id,
version,
payload: args[4].as_bytes().to_vec(),
})
}
"ingest-node" => {
if args.len() < 5 {
return Err("usage: ir ingest-node <node_id> <version> <adjacency_csv>".to_string());
}
let node_id = args[2]
.parse::<u64>()
.map_err(|_| "invalid node_id".to_string())?;
let version = args[3]
.parse::<u64>()
.map_err(|_| "invalid version".to_string())?;
let adjacency = parse_csv_u64(&args[4])?;
Ok(Command::IngestNode {
node_id,
version,
adjacency,
})
}
"ingest-batch-edge" => {
if args.len() < 6 {
return Err(
"usage: ir ingest-batch-edge <start_node_id> <count> <version> <payload_prefix>"
.to_string(),
);
}
let start_node_id = args[2]
.parse::<u64>()
.map_err(|_| "invalid start_node_id".to_string())?;
let count = args[3]
.parse::<u64>()
.map_err(|_| "invalid count".to_string())?;
let version = args[4]
.parse::<u64>()
.map_err(|_| "invalid version".to_string())?;
Ok(Command::IngestBatchEdge {
start_node_id,
count,
version,
payload_prefix: args[5].clone(),
})
}
"ingest-batch-edge-loop" => {
if args.len() < 7 {
return Err(
"usage: ir ingest-batch-edge-loop <start_node_id> <count> <rounds> <version> <payload_prefix>"
.to_string(),
);
}
let start_node_id = args[2]
.parse::<u64>()
.map_err(|_| "invalid start_node_id".to_string())?;
let count = args[3]
.parse::<u64>()
.map_err(|_| "invalid count".to_string())?;
let rounds = args[4]
.parse::<u64>()
.map_err(|_| "invalid rounds".to_string())?;
let version = args[5]
.parse::<u64>()
.map_err(|_| "invalid version".to_string())?;
Ok(Command::IngestBatchEdgeLoop {
start_node_id,
count,
rounds,
version,
payload_prefix: args[6].clone(),
})
}
"ingest" => {
let mut path: Option<PathBuf> = None;
let mut batch_size: usize = 1024;
let mut idx = 2;
while idx < args.len() {
match args[idx].as_str() {
"--file" => {
if idx + 1 >= args.len() {
return Err("missing value for --file".to_string());
}
path = Some(PathBuf::from(&args[idx + 1]));
idx += 2;
}
"--batch-size" => {
if idx + 1 >= args.len() {
return Err("missing value for --batch-size".to_string());
}
batch_size = args[idx + 1]
.parse::<usize>()
.map_err(|_| "invalid batch size".to_string())?;
idx += 2;
}
other => return Err(format!("unknown ingest option: {}", other)),
}
}
let path = path
.ok_or_else(|| "usage: ir ingest --file <path> [--batch-size <n>]".to_string())?;
if batch_size == 0 {
return Err("batch size must be > 0".to_string());
}
Ok(Command::IngestFile { path, batch_size })
}
"parse" => {
if args.len() < 3 {
return Err("usage: ir parse <query>".to_string());
}
Ok(Command::Parse {
query: args[2..].join(" "),
})
}
"explain" => {
if args.len() < 3 {
return Err("usage: ir explain <query>".to_string());
}
Ok(Command::Explain {
query: args[2..].join(" "),
})
}
"query" => {
if args.len() < 3 {
return Err("usage: ir query <query>".to_string());
}
Ok(Command::Query {
query: args[2..].join(" "),
})
}
"contract-report" => {
if args.len() != 3 {
return Err("usage: ir contract-report <acceptance|retrieval-quality>".to_string());
}
Ok(Command::ContractReport {
scenario: args[2].clone(),
})
}
"evidence-report" => {
if args.len() != 3 {
return Err("usage: ir evidence-report <restart-requery>".to_string());
}
Ok(Command::EvidenceReport {
surface: args[2].clone(),
})
}
"service-report" => {
let mut listen: Option<String> = None;
let mut idx = 2;
while idx < args.len() {
match args[idx].as_str() {
"--listen" => {
if idx + 1 >= args.len() {
return Err("missing value for --listen".to_string());
}
listen = Some(args[idx + 1].clone());
idx += 2;
}
other => return Err(format!("unknown service-report option: {}", other)),
}
}
Ok(Command::ServiceReport { listen })
}
"service-validate" => parse_service_command(args, false),
"service-serve" => parse_service_command(args, true),
"bench" => {
if args.len() < 3 {
return Err(
"usage: ir bench <iterations> [--seed <u64>] [--json <path>]".to_string(),
);
}
let iterations = args[2]
.parse::<u64>()
.map_err(|_| "invalid iterations".to_string())?;
let mut seed = 42_u64;
let mut json_out: Option<PathBuf> = None;
let mut idx = 3;
while idx < args.len() {
match args[idx].as_str() {
"--seed" => {
if idx + 1 >= args.len() {
return Err("missing value for --seed".to_string());
}
seed = args[idx + 1]
.parse::<u64>()
.map_err(|_| "invalid seed".to_string())?;
idx += 2;
}
"--json" => {
if idx + 1 >= args.len() {
return Err("missing value for --json".to_string());
}
json_out = Some(PathBuf::from(&args[idx + 1]));
idx += 2;
}
other => {
return Err(format!("unknown bench option: {}", other));
}
}
}
Ok(Command::Bench {
iterations,
seed,
json_out,
})
}
other => Err(format!("unknown command: {} (run `ir --help`)", other)),
}
}
fn parse_service_command(args: &[String], serve: bool) -> Result<Command, String> {
let mut listen: Option<String> = None;
let mut telemetry_endpoint: Option<String> = None;
let mut tls = "disabled".to_string();
let mut admin_token: Option<String> = None;
let mut max_requests = 0usize;
let mut idx = 2;
while idx < args.len() {
match args[idx].as_str() {
"--listen" => {
if idx + 1 >= args.len() {
return Err("missing value for --listen".to_string());
}
listen = Some(args[idx + 1].clone());
idx += 2;
}
"--telemetry-endpoint" => {
if idx + 1 >= args.len() {
return Err("missing value for --telemetry-endpoint".to_string());
}
telemetry_endpoint = Some(args[idx + 1].clone());
idx += 2;
}
"--tls" => {
if idx + 1 >= args.len() {
return Err("missing value for --tls".to_string());
}
tls = args[idx + 1].clone();
idx += 2;
}
"--admin-token" => {
if idx + 1 >= args.len() {
return Err("missing value for --admin-token".to_string());
}
admin_token = Some(args[idx + 1].clone());
idx += 2;
}
"--max-requests" => {
if idx + 1 >= args.len() {
return Err("missing value for --max-requests".to_string());
}
max_requests = args[idx + 1]
.parse::<usize>()
.map_err(|_| "invalid max_requests".to_string())?;
idx += 2;
}
other => return Err(format!("unknown {} option: {}", args[1], other)),
}
}
if serve {
Ok(Command::ServiceServe {
listen,
telemetry_endpoint,
tls,
admin_token,
max_requests,
})
} else {
Ok(Command::ServiceValidate {
listen,
telemetry_endpoint,
tls,
admin_token,
max_requests,
})
}
}
fn parse_put<F>(args: &[String], ctor: F) -> Result<Command, String>
where
F: FnOnce(u64, u64, Vec<u8>) -> Command,
{
if args.len() < 5 {
return Err("usage: ir put-<edge|vector> <node_id> <version> <payload>".to_string());
}
let node_id = args[2]
.parse::<u64>()
.map_err(|_| "invalid node_id".to_string())?;
let version = args[3]
.parse::<u64>()
.map_err(|_| "invalid version".to_string())?;
let payload = args[4].as_bytes().to_vec();
Ok(ctor(node_id, version, payload))
}
fn parse_csv_u64(input: &str) -> Result<Vec<u64>, String> {
if input.trim().is_empty() {
return Ok(Vec::new());
}
input
.split(',')
.map(|part| {
part.trim()
.parse::<u64>()
.map_err(|_| "invalid adjacency".to_string())
})
.collect()
}