import_modules/
lib.rs

1use std::{env,path::{self, Path}};
2
3use fancy_regex::Regex;
4use proc_macro::TokenStream;
5use serde::Deserialize;
6
7// - - -
8
9#[derive(Clone, Debug, Deserialize)]
10struct JSONConfig {
11    pub directory: String,
12    pub recursive: Option<bool>,
13
14    pub inter_process: Option<String>,
15    pub post_process: Option<String>,
16
17    pub public_module: Option<bool>,
18    pub module: Option<bool>,
19
20    #[serde(default = "Vec::new")]
21    pub exclude_files: Vec<String>,
22
23    #[serde(default = "Vec::new")]
24    pub exclude_dirs: Vec<String>,
25
26    #[serde(default = "Vec::new")]
27    pub include_files: Vec<String>,
28
29    #[serde(default = "Vec::new")]
30    pub include_dirs: Vec<String>,
31}
32
33// - - -
34
35struct Config {
36    /// The directory where the modules are located.
37    pub directory: String,
38
39    /// Recursive search for modules.
40    pub recursive: bool,
41
42    /// Intermediary processes allow you to manipulate how the modules are processed.
43    /// Intermediary processes replaces {} with the module.
44    pub inter_process: Option<String>,
45
46    /// Post processes allow you to manipulate how the modules are processed.
47    /// Post_process replaces {} with the module.
48    pub post_process: Option<String>,
49
50    /// Similar to intermediate process, this imports it by default as a public Rust module.
51    pub public_module: Option<bool>,
52
53    /// Similar to intermediate process, this imports it by default as a Rust module.
54    ///
55    /// Default if you don't use inter_process: true
56    /// Default if you use public_module: true
57    pub module: Option<bool>,
58
59    /// Exclude files from the module list.
60    ///
61    /// Default: ["lib.rs", "main.rs", "mod.rs"]
62    pub exclude_files: Vec<Regex>,
63
64    /// Exclude directories from the module list.
65    ///
66    /// Default: [".git", ".github", "lib", "src" "tests", "target"]
67    /// Note: Only if ends by directory separator.
68    pub exclude_dirs: Vec<Regex>,
69
70    /// Include files from the module list.
71    pub include_files: Vec<Regex>,
72
73    /// Include directories from the module list.
74    pub include_dirs: Vec<Regex>,
75}
76
77// - - -
78
79fn files_from(path: &str, config: &Config) -> Vec<String> {
80    let mut modules = Vec::new();
81
82    for path in std::fs::read_dir(path).unwrap() {
83        let path = path.unwrap().path();
84
85        if path.is_dir() {
86            let path = path.to_str().unwrap();
87
88            if config.include_dirs.iter().any(|pattern| {
89                pattern.is_match(&path).expect(&format!(
90                    "Failed to match include_dirs pattern: {}.",
91                    pattern
92                ))
93            }) {
94                continue;
95            }
96
97            if config.exclude_dirs.iter().any(|pattern| {
98                pattern.is_match(&path).expect(&format!(
99                    "Failed to match exclude_dirs pattern: {}.",
100                    pattern
101                ))
102            }) {
103                continue;
104            }
105
106            if config.recursive {
107                modules.extend(files_from(path, config));
108            }
109
110            modules.push(path.to_string());
111        } else {
112            let path = path.to_str().unwrap();
113
114            if config.include_files.iter().any(|pattern| {
115                !pattern.is_match(&path).expect(&format!(
116                    "Failed to match include_files pattern: {}.",
117                    pattern
118                ))
119            }) {
120                continue;
121            }
122
123            if config.exclude_files.iter().any(|pattern| {
124                pattern.is_match(&path).expect(&format!(
125                    "Failed to match exclude_files pattern: {}.",
126                    pattern
127                ))
128            }) {
129                continue;
130            }
131
132            modules.push(path.to_string());
133        }
134    }
135
136    modules
137}
138
139// - - -
140
141fn parse_modules(base: &str, config: &Config, data: Vec<String>) -> Vec<String> {
142    let mut modules = Vec::new();
143
144    for module in data {
145        let module = module.replace(base, "");
146
147        // Maybe conflict with escape separators.
148        let module = module.replace(path::MAIN_SEPARATOR_STR, "::");
149        let module = module.replace(".rs", "");
150
151        if module.len() == 0 {
152            continue;
153        }
154
155        let mut module = module;
156
157        if let Some(inter) = &config.inter_process {
158            if let Some(state) = config.public_module {
159                if state {
160                    panic!("Cannot use public_module and process_as at the same time.");
161                }
162
163            }
164
165            if let Some(state) = config.module {
166                if state {
167                    panic!("Cannot use module and process_as at the same time.");
168                }
169            }
170
171            module = inter.replace("{}", &module);
172        } else {
173            let pub_module_state = config.public_module.unwrap_or(false);
174            let module_state = config.module.unwrap_or(true);
175
176            if pub_module_state && module_state {
177                module.insert_str(0, "pub mod ");
178            } else if module_state {
179                module.insert_str(0, "mod ");
180            } else {
181                panic!("You must set the module to true to use public_module.");
182            }
183
184            module.push(';');
185        }
186
187        modules.push(module);
188    }
189
190    modules
191}
192
193// - - -
194
195/// Returns a list of modules processed based on a configuration.
196///
197/// # Arguments
198///
199/// * `input` - A JSON containing the configuration.
200///
201/// # Example
202///
203/// This is based on the [import-modules](https://github.com/FlamesX-128/import-modules) test directory.
204///
205/// ## Inter Process and Post Process
206///
207/// Intermediary processes allow you to manipulate how the modules are processed.
208///
209/// ### Input
210///
211/// ```rust, ignore
212/// use import_macro::import;
213///
214/// // The {} is replaced by the module.
215/// let functions = import!({
216///     "directory": "tests/math/",
217///     "inter_process": "math::{}::handler,",
218///     "post_process": "vec![{}]"
219/// });
220/// ```
221///
222/// ### Output
223///
224/// ```rust, ignore
225/// let functions = vec![
226///     math::add::handler,
227///     math::sub::handler,
228/// ];
229/// ```
230///
231/// ## Module
232///
233/// Similar to intermediate process, this imports it by default as Rust module.
234///
235/// ### Input
236///
237/// ```rust, ignore
238/// use import_macro::import;
239///
240/// import!({
241///     "directory": "tests",
242/// });
243/// ```
244///
245/// ### Output
246///
247/// ```rust, ignore
248/// mod math;
249/// ```
250///
251
252#[proc_macro]
253pub fn import(input: TokenStream) -> TokenStream {
254    let input = input.to_string();
255
256    let mut config = serde_json::from_str::<JSONConfig>(&input)
257        .expect("Failed to parse config.");
258
259    let exc = match env::consts::OS {
260        "windows" => r"(\\)?$",
261        _ => r"(/)?$",
262    };
263
264    // default files and directories excluded.
265    config.exclude_files.push(r"(lib|main|mod).rs$".to_string());
266    config.exclude_dirs
267        .push(("(.git|.github|lib|src|target|tests)".to_string()) + exc);
268
269    config.directory = config.directory.replace("/", path::MAIN_SEPARATOR_STR);
270
271    if !config.directory.ends_with(path::MAIN_SEPARATOR) {
272        config.directory.push(path::MAIN_SEPARATOR);
273    }
274
275    // Build regexes.
276    let config = Config {
277        directory: config.directory,
278        recursive: config.recursive.unwrap_or(false),
279
280        inter_process: config.inter_process,
281        post_process: config.post_process,
282
283        public_module: config.public_module,
284        module: config.module,
285
286        exclude_files: config
287            .exclude_files.iter()
288            .map(|pattern| {
289                Regex::new(pattern).expect(&format!(
290                    "Failed to parse exclude_files pattern: {}.",
291                    pattern
292                ))
293            })
294            .collect(),
295
296        exclude_dirs: config
297            .exclude_dirs.iter()
298            .map(|pattern| {
299                Regex::new(pattern).expect(&format!(
300                    "Failed to parse exclude_dirs pattern: {}.",
301                    pattern
302                ))
303            })
304            .collect(),
305
306        include_files: config
307            .include_files.iter()
308            .map(|pattern| {
309                Regex::new(pattern).expect(&format!(
310                    "Failed to parse include_files pattern: {}.",
311                    pattern
312                ))
313            })
314            .collect(),
315
316        include_dirs: config
317            .include_dirs.iter()
318            .map(|pattern| {
319                Regex::new(pattern).expect(&format!(
320                    "Failed to parse include_dirs pattern: {}.",
321                    pattern
322                ))
323            })
324            .collect(),
325    };
326
327    // This is the directory of the manifest.
328    let manifiest_dir = env::var("CARGO_MANIFEST_DIR")
329        .expect("Failed to get CARGO_MANIFEST_DIR env variable.");
330
331    let path = Path::new(&manifiest_dir)
332        .join(&config.directory);
333
334    let path = path.to_str()
335        .unwrap();
336
337    let output = files_from(&path, &config);
338    let output = parse_modules(&path, &config, output);
339
340    if let Some(post) = &config.post_process {
341        post.replace("{}", &output.join("")).parse().unwrap()
342    } else {
343        output.join("").parse().unwrap()
344    }
345
346}
347