mago_service/source/
mod.rs

1use std::path::Path;
2
3use ahash::HashSet;
4use async_walkdir::Error;
5use async_walkdir::Filtering;
6use async_walkdir::WalkDir;
7use futures::StreamExt;
8
9use mago_interner::ThreadedInterner;
10use mago_source::SourceManager;
11
12use crate::source::config::SourceConfiguration;
13
14pub mod config;
15
16#[derive(Debug)]
17pub struct SourceService {
18    interner: ThreadedInterner,
19    configuration: SourceConfiguration,
20}
21
22impl SourceService {
23    pub fn new(interner: ThreadedInterner, configuration: SourceConfiguration) -> Self {
24        Self { interner, configuration }
25    }
26
27    /// Load the source manager by scanning and processing the sources
28    /// as per the given configuration.
29    ///
30    /// # Returns
31    ///
32    /// A `Result` containing the new source manager or a `SourceError` if
33    /// an error occurred during the build process.
34    pub async fn load(&self) -> Result<SourceManager, Error> {
35        let SourceConfiguration { root, paths, includes, excludes, extensions } = &self.configuration;
36
37        let mut starting_paths = Vec::new();
38
39        if paths.is_empty() {
40            starting_paths.push((root.clone(), true));
41        } else {
42            for source in paths {
43                starting_paths.push((source.clone(), true));
44            }
45        }
46
47        for include in includes {
48            starting_paths.push((include.clone(), false));
49        }
50
51        if paths.is_empty() && includes.is_empty() {
52            starting_paths.push((root.clone(), true));
53        }
54
55        let excludes_set: HashSet<&String> = excludes.iter().collect();
56        let extensions: HashSet<&String> = extensions.iter().collect();
57
58        let manager = SourceManager::new(self.interner.clone());
59        for (path, user_defined) in starting_paths.into_iter() {
60            let mut entries = WalkDir::new(path)
61                // filter out .git directories
62                .filter(|entry| async move {
63                    if entry.path().starts_with(".") {
64                        Filtering::IgnoreDir
65                    } else {
66                        Filtering::Continue
67                    }
68                });
69
70            // Check for errors after processing all entries in the current path
71            while let Some(entry) = entries.next().await {
72                let path = entry?.path();
73
74                if is_excluded(&path, &excludes_set) {
75                    continue;
76                }
77
78                if path.is_file() && is_accepted_file(&path, &extensions) {
79                    let name = match path.strip_prefix(root) {
80                        Ok(rel_path) => rel_path.to_path_buf(),
81                        Err(_) => path.clone(),
82                    };
83
84                    let name_str = name.to_string_lossy().to_string();
85
86                    manager.insert_path(name_str, path.clone(), user_defined);
87                }
88            }
89        }
90
91        Ok(manager)
92    }
93}
94
95fn is_excluded(path: &Path, excludes: &HashSet<&String>) -> bool {
96    excludes.iter().any(|ex| path.ends_with(ex) || glob_match::glob_match(ex, path.to_string_lossy().as_ref()))
97}
98
99fn is_accepted_file(path: &Path, extensions: &HashSet<&String>) -> bool {
100    if extensions.is_empty() {
101        path.extension().and_then(|s| s.to_str()).map(|ext| ext.eq_ignore_ascii_case("php")).unwrap_or(false)
102    } else {
103        path.extension().and_then(|s| s.to_str()).map(|ext| extensions.contains(&ext.to_string())).unwrap_or(false)
104    }
105}