tiny_web_macro/
lib.rs

1extern crate proc_macro;
2
3use proc_macro::{Span, TokenStream};
4use quote::quote;
5use std::{
6    process::Command,
7    collections::{hash_map::Entry, HashMap},
8    env,
9    fs::{read_dir, read_to_string},
10    str::FromStr,
11};
12use syn::{parse_macro_input, Error};
13
14/// Recursively build engine system
15#[proc_macro]
16pub fn addfn(_: TokenStream) -> TokenStream {
17    let dir = match env::var_os("CARGO_MANIFEST_DIR") {
18        Some(d) => match d.to_str() {
19            Some(s) => s.to_owned(),
20            None => return error("CARGO_MANIFEST_DIR contains non-printable characters"),
21        },
22        None => return error("Can't fetch the environment variable CARGO_MANIFEST_DIR"),
23    };
24    let list = load_files(&dir).unwrap_or_default();
25
26    let mut vec = Vec::new();
27    vec.push("let mut app: std::collections::BTreeMap<i64, std::collections::BTreeMap<i64, std::collections::BTreeMap<i64, tiny_web::sys::action::Act>>> = std::collections::BTreeMap::new();"
28            .to_string(),
29    );
30    for (key, v) in list {
31        vec.push(format!(
32            "let mut {}: std::collections::BTreeMap<i64, std::collections::BTreeMap<i64, tiny_web::sys::action::Act>> = std::collections::BTreeMap::new();",
33            key
34        ));
35        for file in v {
36            let func = get_func(&dir, &key, &file);
37            vec.push(format!(
38                "let mut {}_{}: std::collections::BTreeMap<i64, tiny_web::sys::action::Act> = std::collections::BTreeMap::new();",
39                key, file
40            ));
41            for f in func {
42                vec.push(format!(
43                    "{0}_{1}.insert({2}_i64, |action| Box::pin(app::{0}::{1}::{3}(action)));",
44                    key,
45                    file,
46                    fnv1a_64_impl(&f),
47                    f
48                ));
49            }
50            vec.push(format!(
51                "{0}.insert({1}_i64, {0}_{2});",
52                key,
53                fnv1a_64_impl(&file),
54                file
55            ));
56        }
57        vec.push(format!("app.insert({}_i64, {});", fnv1a_64_impl(&key), key));
58    }
59    vec.push("\nreturn app;".to_owned());
60
61    TokenStream::from_str(&vec.join("\n")).unwrap()
62}
63
64/// Gets functions list from directory
65///
66/// Each function have to start `pub async fn ` and finish `( this : &mut Action ) -> Answer {`.
67fn get_func(dir: &str, key: &str, file: &str) -> Vec<String> {
68    let mut vec = Vec::new();
69    let file = format!("{}/src/app/{}/{}.rs", dir, key, file);
70    if let Ok(str) = read_to_string(file) {
71        let mut str = str
72            .replace('(', " ( ")
73            .replace(')', " ) ")
74            .replace(':', " : ")
75            .replace("->", " -> ")
76            .replace('{', " { ");
77        loop {
78            if str.contains("  ") {
79                str = str.replace("  ", " ");
80                continue;
81            }
82            break;
83        }
84        for line in str.lines() {
85            if let Some(i) = line.find("pub async fn ") {
86                if let Some(j) = line[i + 13..].find(" ( ") {
87                    if line[i + 13 + j..].contains(" : &mut Action ) -> Answer {") {
88                        let name = &line[i + 13..i + 13 + j];
89                        if name.chars().all(|c| c.is_ascii_lowercase() || c == '_') {
90                            vec.push(line[i + 13..i + 13 + j].to_owned());
91                        }
92                    }
93                }
94            }
95        }
96    }
97    vec.shrink_to_fit();
98    vec
99}
100
101/// Recursively links all files with `.rs` extension from `./src/app/*` directory.
102#[proc_macro]
103pub fn addmod(_: TokenStream) -> TokenStream {
104    // Get project dir
105    let dir = match env::var_os("CARGO_MANIFEST_DIR") {
106        Some(d) => match d.to_str() {
107            Some(s) => s.to_owned(),
108            None => return error("CARGO_MANIFEST_DIR contains non-printable characters"),
109        },
110        None => return error("Can't fetch the environment variable CARGO_MANIFEST_DIR"),
111    };
112    // load all files
113    let list = match load_files(&dir) {
114        Ok(l) => l,
115        Err(e) => return error(&e),
116    };
117    // Forms an answer
118    let mut vec = Vec::new();
119    for (key, v) in list {
120        vec.push(format!("pub mod {} {{", check_name(key)));
121        for f in v {
122            vec.push(format!("    pub mod {};", check_name(f)));
123        }
124        vec.push("}".to_owned());
125    }
126    TokenStream::from_str(&vec.join("\n")).unwrap()
127}
128
129/// If the name contains the symbol "-", it replaces it with "_"
130fn check_name(text: String) -> String {
131    if text.contains('-') {
132        return text.replace('-', "_");
133    }
134    text
135}
136
137/// Load all file names with `.rs` extension from `./src/app/*` directory
138fn load_files(dir: &str) -> Result<HashMap<String, Vec<String>>, String> {
139    let src = format!("{}/src/app", dir);
140    let mut list: HashMap<String, Vec<String>> = HashMap::new();
141    // Reads dir from first level
142    match read_dir(&src) {
143        Ok(dir) => {
144            for entry in dir.flatten() {
145                let path = entry.path();
146                if !path.is_dir() {
147                    continue;
148                }
149                if let Some(name) = path.file_name() {
150                    let dir_name = match name.to_str() {
151                        Some(n) => n,
152                        None => continue,
153                    };
154                    // Reads dir from second level
155                    let dir = match read_dir(format!("{}/{}", &src, dir_name)) {
156                        Ok(d) => d,
157                        Err(_) => continue,
158                    };
159                    for entry in dir.flatten() {
160                        let path = entry.path();
161                        if !path.is_file() {
162                            continue;
163                        }
164                        let file_name = match path.file_name() {
165                            Some(name) => match name.to_str() {
166                                Some(file_name) => file_name,
167                                None => continue,
168                            },
169                            None => continue,
170                        };
171                        // Checks extension
172                        if file_name.len() > 3 && file_name.ends_with(".rs") {
173                            let file_name = file_name[..file_name.len() - 3].to_owned();
174                            match list.entry(dir_name.to_owned()) {
175                                Entry::Occupied(mut o) => {
176                                    let vec = o.get_mut();
177                                    vec.push(file_name);
178                                    vec.shrink_to_fit();
179                                }
180                                Entry::Vacant(v) => {
181                                    let vec = vec![file_name];
182                                    v.insert(vec);
183                                }
184                            }
185                        }
186                    }
187                }
188            }
189        }
190        Err(e) => return Err(format!("{}. File name: {}", e, src)),
191    };
192    list.shrink_to_fit();
193    Ok(list)
194}
195
196/// Returns error text
197fn error(text: &str) -> TokenStream {
198    TokenStream::from(Error::new(Span::call_site().into(), text).to_compile_error())
199}
200
201/// fnv1a_64 hash function
202///
203/// # Parameters
204///
205/// * `text: &str` - Origin string.
206///
207/// # Return
208///
209/// i64 hash
210#[inline]
211fn fnv1a_64_impl(text: &str) -> i64 {
212    let mut hash: u64 = 0xcbf29ce484222325;
213    let prime: u64 = 0x100000001b3;
214
215    for c in text.bytes() {
216        hash ^= u64::from(c);
217        hash = hash.wrapping_mul(prime);
218    }
219    unsafe { *(&hash as *const u64 as *const i64) }
220}
221
222/// fnv1a_64 hash function
223/// Apply only for static &str
224#[proc_macro]
225pub fn fnv1a_64(params: TokenStream) -> TokenStream {
226    let text = parse_macro_input!(params as syn::LitStr).value();
227    let result = fnv1a_64_impl(&text);
228    let result_token_stream = quote! { #result };
229    result_token_stream.into()
230}
231
232/// Return the version of the Rust compiler
233#[proc_macro]
234pub fn version(_: TokenStream) -> TokenStream {
235    let output = match Command::new("rustc").arg("--version").output() {
236        Ok(output) => output,
237        Err(e) => return error(&format!("Can't execute command `rustc --version`: {}", e)),
238    };
239    let result = String::from_utf8_lossy(&output.stdout);
240    if !result.starts_with("rustc ") {
241        return error(&format!("`rustc --version` returns an incorrect version string: {}", result))
242    }
243
244    let result_token_stream = quote! { #result };
245    result_token_stream.into()
246}