use crate::nesting;
use serde_json::Value;
use std::collections::HashMap;
pub fn parse() -> (Vec<String>, Value) {
parse_from(std::env::args().skip(1))
}
fn parse_from(args: impl Iterator<Item = String>) -> (Vec<String>, Value) {
let args: Vec<String> = args.collect();
let mut positionals = Vec::new();
let mut flat: HashMap<String, Value> = HashMap::new();
let mut i = 0;
while i < args.len() && !args[i].starts_with("--") {
if args[i].starts_with('-') {
eprintln!(
"error: short-form flags are not supported: '{}'; use --key value or --key=value",
args[i]
);
std::process::exit(1);
}
positionals.push(args[i].clone());
i += 1;
}
while i < args.len() {
let arg = &args[i];
if let Some(rest) = arg.strip_prefix("--") {
if let Some((key, value)) = rest.split_once('=') {
let parsed = serde_json::from_str(value)
.unwrap_or_else(|_| Value::String(value.to_string()));
flat.insert(key.to_string(), parsed);
i += 1;
} else {
let key = rest.to_string();
i += 1;
if i >= args.len() || args[i].starts_with('-') {
eprintln!(
"error: flag '--{key}' requires a value (use --{key}=<value> or --{key} <value>)"
);
std::process::exit(1);
}
let parsed = serde_json::from_str(&args[i])
.unwrap_or_else(|_| Value::String(args[i].clone()));
flat.insert(key, parsed);
i += 1;
}
} else {
eprintln!(
"error: positional argument '{}' must come before all flags",
arg
);
std::process::exit(1);
}
}
(positionals, nesting::nest(flat))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn args(a: &[&str]) -> impl Iterator<Item = String> {
a.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>()
.into_iter()
}
#[test]
fn empty_args() {
let (positionals, flags) = parse_from(args(&[]));
assert!(positionals.is_empty());
assert_eq!(flags, json!({}));
}
#[test]
fn only_positionals() {
let (positionals, flags) = parse_from(args(&["step1", "step2"]));
assert_eq!(positionals, vec!["step1", "step2"]);
assert_eq!(flags, json!({}));
}
#[test]
fn flag_equals_form() {
let (_, flags) = parse_from(args(&["--host=localhost"]));
assert_eq!(flags["host"], json!("localhost"));
}
#[test]
fn flag_space_form() {
let (_, flags) = parse_from(args(&["--host", "localhost"]));
assert_eq!(flags["host"], json!("localhost"));
}
#[test]
fn positionals_and_flags() {
let (positionals, flags) = parse_from(args(&["run", "--port", "8080"]));
assert_eq!(positionals, vec!["run"]);
assert_eq!(flags["port"], json!(8080));
}
#[test]
fn nested_key() {
let (_, flags) = parse_from(args(&["--server.port", "9000"]));
assert_eq!(flags["server"]["port"], json!(9000));
}
#[test]
fn json_bool_parsed() {
let (_, flags) = parse_from(args(&["--debug", "true"]));
assert_eq!(flags["debug"], json!(true));
}
#[test]
fn non_json_kept_as_string() {
let (_, flags) = parse_from(args(&["--name", "my app"]));
assert_eq!(flags["name"], json!("my app"));
}
#[test]
fn multiple_flags() {
let (_, flags) = parse_from(args(&["--host", "localhost", "--port=9000"]));
assert_eq!(flags["host"], json!("localhost"));
assert_eq!(flags["port"], json!(9000));
}
}