use crate::codes::Code;
use std::fmt;
use crate::project::{Project, REPORTS_DIR, Report, dotted};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SelectorError {
Ambiguous {
token: String,
candidates: Vec<Vec<String>>,
},
NoMatch {
token: String,
},
UnknownSource {
token: String,
message: String,
},
}
impl SelectorError {
pub fn code(&self) -> Code {
match self {
SelectorError::Ambiguous { .. } => Code::AmbiguousSelector,
SelectorError::NoMatch { .. } => Code::SelectorMatchesNothing,
SelectorError::UnknownSource { .. } => Code::UnknownSource,
}
}
}
impl fmt::Display for SelectorError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SelectorError::Ambiguous { token, candidates } => {
writeln!(
f,
"\"{token}\" matches more than one location — use the dotted form to disambiguate:"
)?;
let width = candidates.iter().map(|c| dotted(c).len()).max().unwrap_or(0);
for (i, c) in candidates.iter().enumerate() {
let d = dotted(c);
write!(f, " {d:<width$} ({REPORTS_DIR}/{}/)", c.join("/"))?;
if i + 1 < candidates.len() {
writeln!(f)?;
}
}
Ok(())
}
SelectorError::NoMatch { token } => {
write!(f, "selector `{token}` matches no report name, tag or folder")
}
SelectorError::UnknownSource { token, message } => write!(f, "selector `{token}`: {message}"),
}
}
}
impl std::error::Error for SelectorError {}
pub fn resolve<'p>(project: &'p Project, selection: &str) -> Result<Vec<&'p Report>, SelectorError> {
let mut picked: Vec<&'p Report> = Vec::new();
for term in selection.split(|c: char| c.is_whitespace() || c == ',' || c == ';') {
if term.is_empty() {
continue;
}
for r in resolve_one(project, term)? {
if !picked.iter().any(|x| x.name == r.name) {
picked.push(r);
}
}
}
picked.sort_by_key(|r| project.reports.iter().position(|x| x.name == r.name));
Ok(picked)
}
fn resolve_one<'p>(project: &'p Project, token: &str) -> Result<Vec<&'p Report>, SelectorError> {
let token = token.trim();
let under = |folder: &[String]| -> Vec<&'p Report> {
project
.reports
.iter()
.filter(|r| r.folder.starts_with(folder))
.collect()
};
if let Some(sel) = token.strip_prefix("source:") {
return by_source(project, token, sel);
}
if let Some(tag) = token.strip_prefix("tag:") {
return Ok(project
.reports
.iter()
.filter(|r| r.tags.iter().any(|t| t == tag))
.collect());
}
if let Some(r) = project.report(token) {
return Ok(vec![r]);
}
if token.contains('.') {
let path: Vec<String> = token.split('.').map(str::to_string).collect();
return Ok(under(&path));
}
let tagged: Vec<&Report> = project
.reports
.iter()
.filter(|r| r.tags.iter().any(|t| t == token))
.collect();
if !tagged.is_empty() {
return Ok(tagged);
}
let folders: Vec<&Vec<String>> = project
.folders
.iter()
.filter(|f| f.last().is_some_and(|l| l == token))
.collect();
match folders.len() {
0 => Err(SelectorError::NoMatch {
token: token.to_string(),
}),
1 => Ok(under(folders[0])),
_ => Err(SelectorError::Ambiguous {
token: token.to_string(),
candidates: folders.into_iter().cloned().collect(),
}),
}
}
fn by_source<'p>(project: &'p Project, token: &str, sel: &str) -> Result<Vec<&'p Report>, SelectorError> {
let unknown = |message: String| SelectorError::UnknownSource {
token: token.to_string(),
message,
};
let (source, table) = match sel.split_once('.') {
Some((s, t)) => (s, Some(t)),
None => (sel, None),
};
let Some(def) = project.sources.get(source) else {
let known: Vec<&str> = project.sources.keys().map(String::as_str).collect();
return Err(unknown(if known.is_empty() {
format!("no source `{source}`: the project declares no `sources:`")
} else {
format!("no source `{source}` (declared: {})", known.join(", "))
}));
};
if let Some(t) = table
&& def.table(t).is_none()
{
return Err(unknown(format!(
"source `{source}` has no table `{t}` (it has: {})",
def.tables
.iter()
.map(|t| t.name.as_str())
.collect::<Vec<_>>()
.join(", ")
)));
}
let reads = |key: &str| match table {
Some(t) => key == format!("{source}.{t}"),
None => key.split_once('.').is_some_and(|(s, _)| s == source),
};
Ok(project
.reports
.iter()
.filter(|r| {
r.bindings
.iter()
.filter_map(|b| b.parsed.as_ref())
.any(|p| p.source_keys().into_iter().any(reads))
})
.collect())
}