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,
}
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,
"--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");
}
}
_ => 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)
}