Skip to main content

include_folder_macro/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::{Ident, Span};
3use quote::quote;
4use std::ffi::OsStr;
5use std::fs;
6use std::path::{Path, PathBuf};
7use syn::{LitStr, parse_macro_input};
8
9#[proc_macro]
10pub fn include_all_modules(input: TokenStream) -> TokenStream {
11    let path_lit = parse_macro_input!(input as LitStr);
12    let path_str = path_lit.value();
13
14    let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
15    let root_path = PathBuf::from(manifest_dir).join(&path_str);
16
17    if !root_path.is_dir() {
18        let error_msg = format!("Path '{}' is not an valid module", root_path.display());
19        return syn::Error::new_spanned(path_lit, error_msg)
20            .to_compile_error()
21            .into();
22    }
23
24    // ignore lib.rs, main.rs, 和 mod.rs
25    let ignore_list = &["lib.rs", "main.rs", "mod.rs"];
26
27    match generate_modules_recursive(&root_path, ignore_list) {
28        Ok(tokens) => tokens.into(),
29        Err(e) => e,
30    }
31}
32
33#[proc_macro]
34pub fn include_folder(input: TokenStream) -> TokenStream {
35    let path_lit = parse_macro_input!(input as LitStr);
36    let path_str = path_lit.value();
37
38    let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
39    let mut root_path = PathBuf::from(manifest_dir);
40    root_path.push(&path_str);
41
42    let module_name = Path::new(&path_str)
43        .file_name()
44        .and_then(|s| s.to_str())
45        .map(|s| s.replace(".", "_"))
46        .expect("Path should contains an valid module");
47
48    if !root_path.is_dir() {
49        let error_msg = format!("Path '{}' is not an valid dir", root_path.display());
50        return syn::Error::new_spanned(path_lit, error_msg)
51            .to_compile_error()
52            .into();
53    }
54
55    let modules = match generate_modules_recursive(&root_path, &["mod.rs"]) {
56        Ok(tokens) => tokens,
57        Err(e) => return e.into(),
58    };
59
60    let top_module_ident = Ident::new(&module_name, Span::call_site());
61    let expanded = quote! {
62        pub mod #top_module_ident {
63            #modules
64        }
65    };
66
67    expanded.into()
68}
69
70fn generate_modules_recursive(
71    dir: &Path,
72    files_to_ignore: &[&str],
73) -> Result<proc_macro2::TokenStream, TokenStream> {
74    let mut modules = Vec::new();
75
76    let entries = match fs::read_dir(dir) {
77        Ok(entries) => entries,
78        Err(e) => {
79            return Err(to_compile_error(format!(
80                "Cannot read dir '{}': {}",
81                dir.display(),
82                e
83            )));
84        }
85    };
86
87    for entry in entries {
88        let entry = entry.map_err(|e| to_compile_error(e.to_string()))?;
89        let path = entry.path();
90        let file_name = entry.file_name();
91        let file_name_str = file_name.to_string_lossy();
92
93        if files_to_ignore.contains(&file_name_str.as_ref()) {
94            continue;
95        }
96
97        if path.is_dir() {
98            let inner_mods = generate_modules_recursive(&path, &["mod.rs"])?;
99
100            if !inner_mods.is_empty() {
101                let mod_name = Ident::new(&file_name_str, Span::call_site());
102                modules.push(quote! {
103                    pub mod #mod_name {
104                        #inner_mods
105                    }
106                });
107            }
108        } else if path.is_file() {
109            if path.extension() == Some(OsStr::new("rs")) {
110                let mod_name_str = path.file_stem().unwrap().to_string_lossy();
111                let mod_name = Ident::new(&mod_name_str, Span::call_site());
112                modules.push(quote! {
113                    pub mod #mod_name;
114                });
115            }
116        }
117    }
118
119    Ok(quote! { #(#modules)* })
120}
121
122fn to_compile_error(msg: String) -> TokenStream {
123    syn::Error::new(Span::call_site(), msg)
124        .to_compile_error()
125        .into()
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use quote::quote;
132    use std::fs;
133    use tempfile::tempdir;
134
135    fn assert_tokens_equal(expected: proc_macro2::TokenStream, actual: proc_macro2::TokenStream) {
136        assert_eq!(expected.to_string(), actual.to_string());
137    }
138
139    #[test]
140    fn test_include_folder_functionality() {
141        let dir = tempdir().expect("Failed to create temp dir");
142        let root = dir.path();
143
144        fs::write(root.join("code.rs"), "").unwrap();
145        let nested_dir = root.join("nested");
146        fs::create_dir(&nested_dir).unwrap();
147        fs::write(nested_dir.join("deep.rs"), "").unwrap();
148        fs::write(root.join("mod.rs"), "// should be ignored").unwrap();
149
150        let generated_tokens = generate_modules_recursive(root, &["mod.rs"]).unwrap();
151
152        let expected_tokens = quote! {
153            pub mod nested {
154                pub mod deep;
155            }
156            pub mod code;
157        };
158        assert_tokens_equal(expected_tokens, generated_tokens);
159    }
160
161    #[test]
162    fn test_include_all_modules_functionality() {
163        let dir = tempdir().expect("Failed to create temp dir");
164        let src_root = dir.path();
165
166        fs::write(src_root.join("lib.rs"), "").unwrap(); // should be ignored
167        fs::write(src_root.join("main.rs"), "").unwrap(); // should be ignored
168        fs::write(src_root.join("mod.rs"), "").unwrap(); // should be included
169        fs::write(src_root.join("api.rs"), "").unwrap(); // should be included
170
171        let utils_dir = src_root.join("utils");
172        fs::create_dir(&utils_dir).unwrap();
173        fs::write(utils_dir.join("string_helpers.rs"), "").unwrap(); // should_be_included
174        fs::write(utils_dir.join("lib.rs"), "").unwrap();
175
176        let generated_tokens =
177            generate_modules_recursive(src_root, &["lib.rs", "main.rs", "mod.rs"]).unwrap();
178
179        let expected_tokens = quote! {
180            pub mod utils {
181                pub mod lib;
182                pub mod string_helpers;
183            }
184            pub mod api;
185        };
186        assert_tokens_equal(expected_tokens, generated_tokens);
187    }
188}