hotspots_discovery/
lib.rs

1//! This crate is used to discover files in a project for being used in the git-hotspots crate.
2#![warn(missing_docs)]
3use log::debug;
4use rayon::prelude::*;
5use std::{ops::Not, path::Path, result, time::Instant};
6use walkdir::{DirEntry, WalkDir};
7
8/// Contains the supported languages. The Undefined variant is used when the language is not
9/// supported.
10#[derive(Debug, Eq, PartialEq)]
11pub enum Lang {
12    /// Variant for the Rust language.
13    Rust,
14    /// Variant for the Go language.
15    Go,
16    /// Variant for the Lua language.
17    Lua,
18    /// Variant for unsupported languages.
19    Undefined,
20}
21
22impl From<&str> for Lang {
23    /// Converts id of the language detected by the detect_lang crate to the Lang enum.
24    fn from(value: &str) -> Self {
25        match value {
26            "go" => Lang::Go,
27            "rust" => Lang::Rust,
28            "lua" => Lang::Lua,
29            _ => Lang::Undefined,
30        }
31    }
32}
33
34/// Contains the path and the language of a file.
35#[derive(Debug, Eq, PartialEq)]
36pub struct File {
37    /// Path of the file.
38    pub path: String,
39    /// Language of the file.
40    pub lang: Lang,
41}
42
43/// Discovery finds files in a directory recursively. It can filter out the files based on their
44/// prefix and if they not contain a certain string.
45#[derive(Default)]
46pub struct Discovery {
47    prefixes: Vec<String>,
48    not_contains: Vec<String>,
49}
50
51impl Discovery {
52    /// Conditions the discovery to only find files that start with the given prefix.
53    pub fn with_prefix(&mut self, p: String) {
54        self.prefixes.push(p);
55    }
56    /// Conditions the discovery to only find files that do not contain the given string.
57    pub fn not_contains(&mut self, p: String) {
58        self.not_contains.push(p);
59    }
60
61    /// Discovers files in the given path. It filters out files that match the conditions set by
62    /// the `with_prefix` and `not_contains` methods.
63    pub fn discover<P: AsRef<Path>>(&self, path: P) -> Option<Vec<File>> {
64        let start = Instant::now();
65        let res: Vec<File> = WalkDir::new(&path)
66            .into_iter()
67            .par_bridge()
68            .filter_map(result::Result::ok)
69            .filter(|p| {
70                if self.prefixes.is_empty() {
71                    true
72                } else {
73                    self.prefixes
74                        .iter()
75                        .any(|prefix| p.path().starts_with(prefix))
76                }
77            })
78            .filter(|p| {
79                if self.not_contains.is_empty() {
80                    true
81                } else {
82                    self.not_contains
83                        .iter()
84                        .any(|prefix| p.path().to_str().unwrap_or("").contains(prefix))
85                        .not()
86                }
87            })
88            .filter(is_project_file)
89            .filter_map(|p| {
90                let lang = match detect_lang::from_path(p.path()) {
91                    Some(lang) => lang.id().into(),
92                    None => Lang::Undefined,
93                };
94                let p = p.path().to_str();
95
96                p.map(|path| File {
97                    path: path.to_owned(),
98                    lang,
99                })
100            })
101            .collect();
102
103        debug!("Discovery took {:?}", start.elapsed());
104
105        if res.is_empty() {
106            None
107        } else {
108            Some(res)
109        }
110    }
111}
112
113/// Checks if the given entry is a non-hidden file and not a directory.
114fn is_project_file(entry: &DirEntry) -> bool {
115    if entry.file_type().is_dir() {
116        return false;
117    }
118    entry
119        .file_name()
120        .to_str()
121        .map(|s| !s.starts_with('.'))
122        .unwrap_or(false)
123}
124
125#[cfg(test)]
126mod tests;