assemble_core/project/
finder.rs

1//! Helpers for subproject handling
2
3use crate::identifier::{TaskId, ID_SEPARATOR};
4use crate::prelude::ProjectId;
5use crate::project;
6use crate::project::shared::SharedProject;
7use crate::project::{GetProjectId, ProjectError, ProjectResult};
8use crate::task::HasTaskId;
9use itertools::Itertools;
10use std::borrow::Borrow;
11use std::collections::VecDeque;
12use std::fmt::{Display, Formatter};
13use std::iter::FusedIterator;
14use std::mem::transmute;
15use std::ops::Deref;
16
17/// Finds a sub project.
18///
19/// # Project Resolution
20/// From a given *base* project, a path is resolved as follows:
21/// - an empty path (`""`) refers to the base project.
22/// - a path starting with a `":"` searches starting from the `root` project.
23/// - A path starting with `:<root project name>` is equivalent to just `":"`.
24/// - each component after that, which are seperated by a `":"`, is a child of the current search
25///
26/// For example, in this given structure with a search starting pointed marked with `<--`,
27/// ```text
28/// root:
29///   - child1: <--
30///     - child2:
31///   - child3:
32/// ```
33///
34/// To access `child2`, you can either use `child2`, `:child1:child2`, or `:root:child1:child2`.
35/// Meanwhile, `child3` can only be accessed via `:child3`, or `:root:child3`.
36#[derive(Debug)]
37pub struct ProjectFinder {
38    project: SharedProject,
39}
40
41impl ProjectFinder {
42    /// Creates a new project finder
43    pub fn new(project: &SharedProject) -> Self {
44        Self {
45            project: project.clone(),
46        }
47    }
48    /// Tries to find a project relative to this one from a project id.
49    ///
50    /// For more info on how finding works, check out the definition of the [`ProjectFinder`](ProjectFinder)
51    pub fn find<S: AsRef<ProjectPath>>(&self, id: S) -> Option<SharedProject> {
52        let path = id.as_ref();
53
54        let mut project_ptr = Some(self.project.clone());
55        let mut at_root = |ptr: &Option<SharedProject>| -> bool {
56            ptr.as_ref().map(|p| p.is_root()).unwrap_or(false)
57        };
58
59        for component in path.components() {
60            match component {
61                PathComponent::Root => {
62                    project_ptr = Some(self.project.with(|p| p.root_project()));
63                }
64                PathComponent::Normal(normal) => {
65                    if at_root(&project_ptr) && project_ptr.as_ref().unwrap().project_id() == normal
66                    {
67                        continue;
68                    }
69                    project_ptr = project_ptr.and_then(|s| s.get_subproject(normal).ok());
70                }
71            }
72        }
73
74        project_ptr
75    }
76}
77
78/// Represents a path to a project
79#[derive(Debug, Eq, PartialEq, Hash)]
80#[repr(transparent)]
81pub struct ProjectPath {
82    path: str,
83}
84
85impl ProjectPath {
86    /// Create a new path from a string
87    pub fn new(path: &str) -> &Self {
88        unsafe { transmute(path) }
89    }
90
91    /// Checks whether this an empty path
92    pub fn is_empty(&self) -> bool {
93        self.components().count() == 0
94    }
95
96    /// Gets the components of the path
97    pub fn components(&self) -> PathComponents<'_> {
98        let mut comp = vec![];
99        let mut path = &self.path;
100        if path.starts_with(ID_SEPARATOR) {
101            comp.push(PathComponent::Root);
102            path = &path[1..];
103        }
104
105        comp.extend(
106            path.split_terminator(ID_SEPARATOR)
107                .map(|s| PathComponent::Normal(s)),
108        );
109
110        PathComponents {
111            comps: comp,
112            index: 0,
113        }
114    }
115}
116
117impl AsRef<ProjectPath> for ProjectPath {
118    fn as_ref(&self) -> &ProjectPath {
119        self
120    }
121}
122
123impl Borrow<ProjectPath> for ProjectPathBuf {
124    fn borrow(&self) -> &ProjectPath {
125        self.as_ref()
126    }
127}
128
129impl ToOwned for ProjectPath {
130    type Owned = ProjectPathBuf;
131
132    fn to_owned(&self) -> Self::Owned {
133        ProjectPathBuf::new(self.path.to_string())
134    }
135}
136
137impl<'a> IntoIterator for &'a ProjectPath {
138    type Item = PathComponent<'a>;
139    type IntoIter = PathComponents<'a>;
140
141    fn into_iter(self) -> Self::IntoIter {
142        self.components()
143    }
144}
145
146/// A component of a project path
147#[derive(Debug, Clone, Eq, PartialEq)]
148pub enum PathComponent<'a> {
149    Root,
150    Normal(&'a str),
151}
152
153/// An owned version of the project path
154#[derive(Debug, Clone, Eq, PartialEq, Hash)]
155pub struct ProjectPathBuf {
156    string: String,
157}
158
159impl ProjectPathBuf {
160    /// creates a new project path
161    pub fn new(path: String) -> ProjectPathBuf {
162        ProjectPathBuf { string: path }
163    }
164}
165
166impl From<&ProjectPath> for ProjectPathBuf {
167    fn from(value: &ProjectPath) -> Self {
168        ProjectPathBuf::new(value.path.to_string())
169    }
170}
171
172impl AsRef<ProjectPath> for ProjectPathBuf {
173    fn as_ref(&self) -> &ProjectPath {
174        ProjectPath::new(&self.string)
175    }
176}
177
178impl AsRef<ProjectPath> for &str {
179    fn as_ref(&self) -> &ProjectPath {
180        ProjectPath::new(*self)
181    }
182}
183
184impl AsRef<ProjectPath> for String {
185    fn as_ref(&self) -> &ProjectPath {
186        ProjectPath::new(&self)
187    }
188}
189
190impl<S: AsRef<str>> From<S> for ProjectPathBuf {
191    fn from(value: S) -> Self {
192        Self::new(value.as_ref().to_string())
193    }
194}
195
196impl From<ProjectId> for ProjectPathBuf {
197    fn from(value: ProjectId) -> Self {
198        ProjectPathBuf::new(value.to_string())
199    }
200}
201
202impl Deref for ProjectPathBuf {
203    type Target = ProjectPath;
204
205    fn deref(&self) -> &Self::Target {
206        self.as_ref()
207    }
208}
209
210/// An iterator over the components of the project path
211#[derive(Debug)]
212pub struct PathComponents<'a> {
213    comps: Vec<PathComponent<'a>>,
214    index: usize,
215}
216
217impl<'a> Iterator for PathComponents<'a> {
218    type Item = PathComponent<'a>;
219
220    fn next(&mut self) -> Option<Self::Item> {
221        let out = self.comps.get(self.index).cloned();
222        if out.is_some() {
223            self.index += 1;
224        }
225        out
226    }
227}
228
229impl FusedIterator for PathComponents<'_> {}
230
231/// Represents a path to a task
232#[derive(Debug, Eq, PartialEq, Hash)]
233#[repr(transparent)]
234pub struct TaskPath {
235    path: str,
236}
237
238impl TaskPath {
239    /// Create a new task path from a string
240    pub fn new(path: &str) -> &Self {
241        unsafe { transmute(path) }
242    }
243
244    fn split(&self) -> (&ProjectPath, &str) {
245        let mut sep: VecDeque<&str> = self.path.rsplitn(2, ID_SEPARATOR).collect();
246        let task = sep.pop_front().expect("one always expected");
247        if let Some(rest) = sep.pop_front() {
248            let project_path = if rest.is_empty() {
249                ProjectPath::new(":")
250            } else {
251                ProjectPath::new(rest)
252            };
253            (project_path, task)
254        } else {
255            (ProjectPath::new(""), task)
256        }
257    }
258
259    /// Gets the project part of the task path, if it exists.
260    pub fn project(&self) -> &ProjectPath {
261        self.split().0
262    }
263
264    /// Gets the task component itself
265    pub fn task(&self) -> &str {
266        self.split().1
267    }
268}
269
270impl ToOwned for TaskPath {
271    type Owned = TaskPathBuf;
272
273    fn to_owned(&self) -> Self::Owned {
274        TaskPathBuf::from(self)
275    }
276}
277
278impl Borrow<TaskPath> for TaskPathBuf {
279    fn borrow(&self) -> &TaskPath {
280        self.as_ref()
281    }
282}
283
284impl AsRef<TaskPath> for TaskPath {
285    fn as_ref(&self) -> &TaskPath {
286        self
287    }
288}
289impl AsRef<str> for TaskPath {
290    fn as_ref(&self) -> &str {
291        &self.path
292    }
293}
294
295impl Display for TaskPath {
296    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
297        write!(f, "{}", &self.path)
298    }
299}
300
301/// An owned version of a [`TaskPath`](TaskPath).
302#[derive(Debug, Clone, Eq, PartialEq, Hash)]
303pub struct TaskPathBuf {
304    src: String,
305}
306
307impl TaskPathBuf {
308    /// Create a new task path buf from a string
309    pub fn new(src: String) -> Self {
310        Self { src }
311    }
312}
313
314impl AsRef<str> for TaskPathBuf {
315    fn as_ref(&self) -> &str {
316        &self.src
317    }
318}
319
320impl AsRef<TaskPath> for TaskPathBuf {
321    fn as_ref(&self) -> &TaskPath {
322        TaskPath::new(&self.src)
323    }
324}
325
326impl AsRef<TaskPath> for &str {
327    fn as_ref(&self) -> &TaskPath {
328        TaskPath::new(*self)
329    }
330}
331
332impl AsRef<TaskPath> for String {
333    fn as_ref(&self) -> &TaskPath {
334        TaskPath::new(&self)
335    }
336}
337
338impl From<&TaskPath> for TaskPathBuf {
339    fn from(value: &TaskPath) -> Self {
340        TaskPathBuf::new(value.path.to_string())
341    }
342}
343
344impl From<String> for TaskPathBuf {
345    fn from(value: String) -> Self {
346        Self::new(value)
347    }
348}
349
350impl From<&str> for TaskPathBuf {
351    fn from(value: &str) -> Self {
352        Self::new(value.to_string())
353    }
354}
355
356impl From<TaskId> for TaskPathBuf {
357    fn from(value: TaskId) -> Self {
358        Self::from(value.to_string())
359    }
360}
361
362impl Deref for TaskPathBuf {
363    type Target = TaskPath;
364
365    fn deref(&self) -> &Self::Target {
366        self.as_ref()
367    }
368}
369
370/// Similar to the project finder, but for tasks.
371#[derive(Debug)]
372pub struct TaskFinder {
373    project: SharedProject,
374}
375
376impl TaskFinder {
377    /// Creates a new task finder from a given base project
378    pub fn new(project: &SharedProject) -> Self {
379        Self {
380            project: project.clone(),
381        }
382    }
383
384    /// Tries to find all tasks
385    pub fn find<T: AsRef<TaskPath>>(&self, task_path: T) -> ProjectResult<Option<Vec<TaskId>>> {
386        let (project, task) = task_path.as_ref().split();
387        trace!(
388            "searching for ({:?}, {:?}) from {}",
389            project,
390            task,
391            self.project
392        );
393        let proj_finder = ProjectFinder::new(&self.project);
394        let proj = proj_finder
395            .find(project)
396            .ok_or(ProjectError::ProjectNotFound(project.to_owned()))?;
397        trace!("found proj: {}", proj);
398
399        let mut output = vec![];
400
401        if let Ok(task_id) = proj.task_id_factory().create(task) {
402            trace!("checking if {} exists", task_id);
403            if let Ok(task) = proj.get_task(&task_id) {
404                if project.is_empty() && !task.only_current() {
405                    output.push(task.task_id());
406                } else {
407                    trace!("exiting immediately with {}", task.task_id());
408                    return Ok(Some(vec![task.task_id()]));
409                }
410            }
411        }
412
413        for registered_task in proj.task_container().get_tasks() {
414            trace!("registered task: {}", registered_task);
415            // todo:
416        }
417
418        if project.is_empty() {
419            self.project.with(|p| {
420                for subproject in p.subprojects() {
421                    let finder = TaskFinder::new(subproject);
422                    if let Ok(Some(tasks)) = finder.find(task) {
423                        output.extend(tasks);
424                    }
425                }
426            });
427        }
428
429        if output.is_empty() {
430            Ok(None)
431        } else {
432            Ok(Some(output))
433        }
434    }
435}
436
437#[cfg(test)]
438mod tests {
439    use super::*;
440    use crate::defaults::tasks::Empty;
441    use crate::project::dev::quick_create;
442    use crate::Project;
443    use std::cell::{Cell, Ref, RefCell};
444    use toml::toml;
445
446    fn init() -> SharedProject {
447        quick_create(
448            r"
449        root:
450          - sub1:
451               - sub2:
452        ",
453        )
454        .expect("couldn't create a project")
455    }
456
457    #[test]
458    fn project_relative_search() {
459        let project = init();
460        let sub1 = project.get_subproject("sub1").unwrap();
461        let finder = ProjectFinder::new(&sub1);
462
463        assert_eq!(finder.find("").unwrap().project_id(), ":root:sub1");
464        assert_eq!(finder.find("sub2").unwrap().project_id(), ":root:sub1:sub2");
465    }
466
467    #[test]
468    fn project_absolute_search() {
469        let project = init();
470        let sub1 = project.get_subproject("sub1").unwrap();
471        let finder = ProjectFinder::new(&sub1);
472        // start from sub 1 to make it more "confirmed"
473
474        assert_eq!(finder.find(":").unwrap().project_id(), ":root");
475        assert_eq!(finder.find(":root").unwrap().project_id(), ":root");
476        assert_eq!(finder.find(":sub1").unwrap().project_id(), ":root:sub1");
477        assert_eq!(
478            finder.find(":sub1:sub2").unwrap().project_id(),
479            ":root:sub1:sub2"
480        );
481        assert!(
482            finder.find(":sub2").is_none(),
483            "sub2 is not a child of the root"
484        );
485    }
486
487    #[test]
488    fn collect_relative_tasks() -> ProjectResult {
489        let project = quick_create(
490            r"
491        parent:
492            - mid1:
493                - child1
494                - child2
495            - mid2:
496                - child4
497    ",
498        )?;
499
500        let mut count = RefCell::new(0_usize);
501        project.allprojects_mut(|project| {
502            project
503                .task_container_mut()
504                .register_task::<Empty>("taskName")
505                .expect("couldnt register task");
506            *count.borrow_mut() += 1;
507        });
508
509        let finder = TaskFinder::new(&project);
510        let found = finder.find("taskName").unwrap().unwrap_or_default();
511        println!("found: {:#?}", found);
512
513        assert_eq!(
514            found.len(),
515            *count.borrow(),
516            "all registered tasks of taskName should be found"
517        );
518
519        Ok(())
520    }
521
522    #[test]
523    fn abs_works() -> ProjectResult {
524        let project = quick_create(
525            r"
526        parent:
527            - mid1:
528                - child1
529                - child2
530    ",
531        )?;
532
533        let mut count = RefCell::new(0_usize);
534        project.allprojects_mut(|project| {
535            project
536                .task_container_mut()
537                .register_task::<Empty>("taskName")
538                .expect("couldnt register task");
539            *count.borrow_mut() += 1;
540        });
541
542        let finder = TaskFinder::new(&project.get_subproject(":mid1")?);
543        let found = finder.find(":taskName").unwrap().unwrap_or_default();
544        println!("found: {:#?}", found);
545
546        assert_eq!(found.len(), 1, "only one returned");
547        assert_eq!(&found[0], &TaskId::new(":parent:taskName").unwrap());
548
549        Ok(())
550    }
551}