1use bids_core::error::{BidsError, Result};
7use bids_core::file::BidsFile;
8use bids_core::utils::get_close_matches;
9use std::path::PathBuf;
10
11use crate::layout::BidsLayout;
12use crate::query::{ReturnType, Scope};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum InvalidFilters {
32 Error,
34 Drop,
36 Allow,
38}
39
40pub struct GetBuilder<'a> {
78 pub(crate) layout: &'a BidsLayout,
79 pub(crate) filters: Vec<(String, Vec<String>, bool)>,
80 pub(crate) return_type: ReturnType,
81 pub(crate) target: Option<String>,
82 pub(crate) scope: Scope,
83 pub(crate) invalid_filters: InvalidFilters,
84}
85
86impl<'a> GetBuilder<'a> {
87 pub(crate) fn new(layout: &'a BidsLayout) -> Self {
88 Self {
89 layout,
90 filters: Vec::new(),
91 return_type: ReturnType::Object,
92 target: None,
93 scope: Scope::All,
94 invalid_filters: InvalidFilters::Error,
95 }
96 }
97
98 #[must_use]
100 pub fn subject(self, v: &str) -> Self {
101 self.filter("subject", v)
102 }
103 #[must_use]
104 pub fn session(self, v: &str) -> Self {
105 self.filter("session", v)
106 }
107 #[must_use]
108 pub fn task(self, v: &str) -> Self {
109 self.filter("task", v)
110 }
111 #[must_use]
112 pub fn run(self, v: &str) -> Self {
113 self.filter("run", v)
114 }
115 #[must_use]
116 pub fn datatype(self, v: &str) -> Self {
117 self.filter("datatype", v)
118 }
119 #[must_use]
120 pub fn acquisition(self, v: &str) -> Self {
121 self.filter("acquisition", v)
122 }
123 #[must_use]
124 pub fn recording(self, v: &str) -> Self {
125 self.filter("recording", v)
126 }
127 #[must_use]
128 pub fn space(self, v: &str) -> Self {
129 self.filter("space", v)
130 }
131 #[must_use]
132 pub fn suffix(self, v: &str) -> Self {
133 self.filter("suffix", v)
134 }
135
136 #[must_use]
137 pub fn extension(self, value: &str) -> Self {
138 let v = if value.starts_with('.') {
139 value.to_string()
140 } else {
141 format!(".{value}")
142 };
143 self.filter_owned("extension", vec![v])
144 }
145
146 #[must_use]
148 pub fn scope(mut self, scope: &str) -> Self {
149 self.scope = Scope::parse(scope);
150 self
151 }
152
153 #[must_use]
155 pub fn invalid_filters(mut self, mode: InvalidFilters) -> Self {
156 self.invalid_filters = mode;
157 self
158 }
159
160 #[must_use]
162 pub fn filter(mut self, entity: &str, value: &str) -> Self {
163 self.filters
164 .push((entity.into(), vec![value.into()], false));
165 self
166 }
167
168 #[must_use]
170 pub fn filter_any(mut self, entity: &str, values: &[&str]) -> Self {
171 self.filters.push((
172 entity.into(),
173 values
174 .iter()
175 .map(std::string::ToString::to_string)
176 .collect(),
177 false,
178 ));
179 self
180 }
181
182 #[must_use]
184 pub fn filter_regex(mut self, entity: &str, pattern: &str) -> Self {
185 self.filters
186 .push((entity.into(), vec![pattern.into()], true));
187 self
188 }
189
190 #[must_use]
192 pub fn query_any(mut self, entity: &str) -> Self {
193 self.filters
194 .push((entity.into(), vec!["__ANY__".into()], false));
195 self
196 }
197
198 #[must_use]
200 pub fn query_none(mut self, entity: &str) -> Self {
201 self.filters
202 .push((entity.into(), vec!["__NONE__".into()], false));
203 self
204 }
205
206 #[must_use]
207 fn filter_owned(mut self, entity: &str, values: Vec<String>) -> Self {
208 self.filters.push((entity.into(), values, false));
209 self
210 }
211
212 #[must_use]
213 pub fn return_filenames(mut self) -> Self {
214 self.return_type = ReturnType::Filename;
215 self
216 }
217 #[must_use]
218 pub fn return_ids(mut self, target: &str) -> Self {
219 self.return_type = ReturnType::Id;
220 self.target = Some(target.into());
221 self
222 }
223 #[must_use]
224 pub fn return_dirs(mut self, target: &str) -> Self {
225 self.return_type = ReturnType::Dir;
226 self.target = Some(target.into());
227 self
228 }
229
230 fn validate_filters(&self) -> Result<Vec<(String, Vec<String>, bool)>> {
233 if self.invalid_filters == InvalidFilters::Allow {
234 return Ok(self.filters.clone());
235 }
236 let entities = self.layout.get_entities()?;
237 let entity_set: std::collections::HashSet<&str> =
238 entities.iter().map(std::string::String::as_str).collect();
239 let mut validated = Vec::new();
240 for (name, values, regex) in &self.filters {
241 if !entity_set.contains(name.as_str()) {
242 match self.invalid_filters {
243 InvalidFilters::Error => {
244 let suggestions = get_close_matches(name, &entities, 3);
245 let mut msg = format!("'{name}' is not a recognized entity.");
246 if !suggestions.is_empty() {
247 msg.push_str(&format!(" Did you mean {suggestions:?}?"));
248 }
249 return Err(BidsError::InvalidFilter(msg));
250 }
251 InvalidFilters::Drop => continue,
252 InvalidFilters::Allow => {}
253 }
254 }
255 validated.push((name.clone(), values.clone(), *regex));
256 }
257 Ok(validated)
258 }
259
260 pub fn collect(self) -> Result<Vec<BidsFile>> {
264 let filters = self.validate_filters()?;
265 let paths = self.layout.query_files_internal(&filters, &self.scope)?;
266 let mut files: Vec<BidsFile> = paths
267 .iter()
268 .filter_map(|p| self.layout.reconstruct_file(p).ok())
269 .collect();
270 files.sort();
271 Ok(files)
272 }
273
274 pub fn return_paths(self) -> Result<Vec<PathBuf>> {
276 let filters = self.validate_filters()?;
277 let paths = self.layout.query_files_internal(&filters, &self.scope)?;
278 let mut result: Vec<PathBuf> = paths.into_iter().map(PathBuf::from).collect();
279 result.sort();
280 Ok(result)
281 }
282
283 pub fn return_unique(self, target: &str) -> Result<Vec<String>> {
285 let filters = self.validate_filters()?;
286 let paths = self.layout.query_files_internal(&filters, &self.scope)?;
287 let mut seen = std::collections::HashSet::new();
288 let mut values = Vec::new();
289 for path in &paths {
290 for (name, value, _, _) in self.layout.db().get_tags(path)? {
291 if name == target && seen.insert(value.clone()) {
292 values.push(value);
293 }
294 }
295 }
296 values.sort();
297 Ok(values)
298 }
299
300 pub fn return_directories(self, target: &str) -> Result<Vec<String>> {
302 let filters = self.validate_filters()?;
303 self.layout.db().query_directories(target, &filters)
304 }
305
306 #[deprecated(since = "0.2.0", note = "renamed to `collect()` for clarity")]
308 pub fn returns(self) -> Result<Vec<BidsFile>> {
309 self.collect()
310 }
311}