use crate::*;
use std::cell::Cell;
thread_local! {
static WARNED: Cell<bool> = const { Cell::new(false) };
}
pub(crate) fn deprecation_note(what: &str) {
WARNED.with(|w| {
if !w.get() {
eprintln!(
"candor-query: {what} is a DEPRECATED grammar (candor-spec §3.3.1) — use \
`--report <locator>`, `--json`, `--policy <file>`; the old positional forms are removed \
at the next major."
);
w.set(true);
}
});
}
pub(crate) fn resolve_locator(loc: &str) -> String {
let p = Path::new(loc);
if p.is_dir() {
return p.join(".candor").join("report").to_string_lossy().into_owned();
}
loc.to_string()
}
pub(crate) fn discover_report_prefix() -> Option<String> {
if let Ok(env) = std::env::var("CANDOR_REPORT")
&& !env.is_empty()
{
return Some(resolve_locator(&env));
}
let mut dir = std::env::current_dir().ok()?;
loop {
if dir.join(".candor").is_dir() {
return Some(dir.join(".candor").join("report").to_string_lossy().into_owned());
}
if !dir.pop() {
return None;
}
}
}
pub(crate) struct Query {
pub(crate) report: Option<String>,
pub(crate) positional: Vec<String>,
pub(crate) want_json: bool,
pub(crate) policy: Option<String>,
pub(crate) strict: bool,
pub(crate) include_unknown: bool,
}
pub(crate) struct Shape {
pub(crate) verb_args: usize,
pub(crate) sentinel: bool,
pub(crate) has_policy: bool,
}
fn did_you_mean_flag(unknown: &str) -> String {
const KNOWN: [&str; 6] = ["--report", "--policy", "--json", "--text", "--strict", "--include-unknown"];
let u = unknown.trim_start_matches('-').to_lowercase();
KNOWN
.iter()
.filter_map(|k| {
let kn = k.trim_start_matches('-');
let shared = u.chars().zip(kn.chars()).take_while(|(a, b)| a == b).count();
(shared >= 3).then_some((shared, *k))
})
.max_by_key(|(s, _)| *s)
.map(|(_, k)| format!(" — did you mean `{k}`?"))
.unwrap_or_default()
}
pub(crate) fn parse(args: &[String], shape: Shape) -> Query {
let mut report: Option<String> = None;
let mut policy: Option<String> = None;
let mut want_json = false;
let mut strict = false;
let mut include_unknown = false;
let mut positional: Vec<String> = Vec::new();
let mut i = 0;
while i < args.len() {
let a = args[i].as_str();
match a {
"--json" => want_json = true,
"--text" | "--human" => {}
"--strict" => strict = true,
"--include-unknown" => include_unknown = true,
"--report" => {
if let Some(v) = args.get(i + 1) {
report = Some(resolve_locator(v));
i += 1;
} else {
eprintln!("candor-query: --report requires a <locator> argument (a directory, a `.json` report file, or a prefix)");
std::process::exit(2);
}
}
"--policy" => {
if let Some(v) = args.get(i + 1) {
policy = Some(v.clone());
i += 1;
} else {
eprintln!("candor-query: --policy requires a <file> argument");
std::process::exit(2);
}
}
other if other.starts_with('-') && other.len() > 1 => {
eprintln!(
"candor-query: unknown flag `{other}`{}\n known flags: --report, --policy, --json, --text, --strict, --include-unknown",
did_you_mean_flag(other)
);
std::process::exit(2);
}
_ => positional.push(args[i].clone()),
}
i += 1;
}
if shape.sentinel
&& positional.len() > shape.verb_args
&& matches!(positional.last().map(String::as_str), Some("0") | Some("1"))
{
deprecation_note("a trailing `0|1` JSON sentinel");
if positional.last().map(String::as_str) == Some("1") {
want_json = true;
}
positional.pop();
}
if shape.has_policy && policy.is_none() && positional.len() > shape.verb_args + 1 {
deprecation_note("a positional policy file");
policy = Some(positional.pop().unwrap());
}
if report.is_none() && positional.len() > shape.verb_args {
deprecation_note("a leading-positional report");
report = Some(resolve_locator(&positional.remove(0)));
}
Query { report, positional, want_json, policy, strict, include_unknown }
}
pub(crate) fn report_or_discover(q: &Query) -> Option<String> {
q.report.clone().or_else(discover_report_prefix)
}