1use std::fmt;
7
8use crate::project::{Project, REPORTS_DIR, Report, dotted};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum SelectorError {
12 Ambiguous {
13 token: String,
14 candidates: Vec<Vec<String>>,
15 },
16 NoMatch {
17 token: String,
18 },
19}
20
21impl SelectorError {
22 pub fn code(&self) -> &'static str {
23 match self {
24 SelectorError::Ambiguous { .. } => "ambiguous-selector",
25 SelectorError::NoMatch { .. } => "selector-matches-nothing",
26 }
27 }
28}
29
30impl fmt::Display for SelectorError {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 match self {
33 SelectorError::Ambiguous { token, candidates } => {
34 writeln!(
35 f,
36 "\"{token}\" matches more than one location — use the dotted form to disambiguate:"
37 )?;
38 let width = candidates.iter().map(|c| dotted(c).len()).max().unwrap_or(0);
39 for (i, c) in candidates.iter().enumerate() {
40 let d = dotted(c);
41 write!(f, " {d:<width$} ({REPORTS_DIR}/{}/)", c.join("/"))?;
42 if i + 1 < candidates.len() {
43 writeln!(f)?;
44 }
45 }
46 Ok(())
47 }
48 SelectorError::NoMatch { token } => {
49 write!(f, "selector `{token}` matches no report name, tag or folder")
50 }
51 }
52 }
53}
54
55impl std::error::Error for SelectorError {}
56
57pub fn resolve<'p>(project: &'p Project, selection: &str) -> Result<Vec<&'p Report>, SelectorError> {
63 let mut picked: Vec<&'p Report> = Vec::new();
64 for term in selection.split(|c: char| c.is_whitespace() || c == ',' || c == ';') {
65 if term.is_empty() {
66 continue;
67 }
68 for r in resolve_one(project, term)? {
69 if !picked.iter().any(|x| x.name == r.name) {
70 picked.push(r);
71 }
72 }
73 }
74 picked.sort_by_key(|r| project.reports.iter().position(|x| x.name == r.name));
75 Ok(picked)
76}
77
78fn resolve_one<'p>(project: &'p Project, token: &str) -> Result<Vec<&'p Report>, SelectorError> {
79 let token = token.trim();
80 let under = |folder: &[String]| -> Vec<&'p Report> {
81 project
82 .reports
83 .iter()
84 .filter(|r| r.folder.starts_with(folder))
85 .collect()
86 };
87 if let Some(tag) = token.strip_prefix("tag:") {
88 return Ok(project
89 .reports
90 .iter()
91 .filter(|r| r.tags.iter().any(|t| t == tag))
92 .collect());
93 }
94 if let Some(r) = project.report(token) {
95 return Ok(vec![r]);
96 }
97 if token.contains('.') {
98 let path: Vec<String> = token.split('.').map(str::to_string).collect();
99 return Ok(under(&path));
100 }
101 let tagged: Vec<&Report> = project
102 .reports
103 .iter()
104 .filter(|r| r.tags.iter().any(|t| t == token))
105 .collect();
106 if !tagged.is_empty() {
107 return Ok(tagged);
108 }
109 let folders: Vec<&Vec<String>> = project
110 .folders
111 .iter()
112 .filter(|f| f.last().is_some_and(|l| l == token))
113 .collect();
114 match folders.len() {
115 0 => Err(SelectorError::NoMatch {
116 token: token.to_string(),
117 }),
118 1 => Ok(under(folders[0])),
119 _ => Err(SelectorError::Ambiguous {
120 token: token.to_string(),
121 candidates: folders.into_iter().cloned().collect(),
122 }),
123 }
124}