Skip to main content

tsync/
lib.rs

1mod to_typescript;
2mod typescript;
3pub mod utils;
4
5use state::InitCell;
6use std::ffi::OsStr;
7use std::fs::File;
8use std::io::{BufRead, BufReader};
9use std::path::{Path, PathBuf};
10use walkdir::{DirEntry, WalkDir};
11
12/// the #[tsync] attribute macro which marks structs and types to be translated into the final typescript definitions file
13pub use tsync_macro::tsync;
14
15use crate::to_typescript::ToTypescript;
16
17pub(crate) static DEBUG: InitCell<bool> = InitCell::new();
18
19/// macro to check from an syn::Item most of them have ident attribs
20/// that is the one we want to print but not sure!
21macro_rules! check_tsync {
22    ($x: ident, in: $y: tt, $z: tt) => {
23        let has_tsync_attribute = has_tsync_attribute(&$x.attrs);
24        if *DEBUG.get() {
25            if has_tsync_attribute {
26                println!("Encountered #[tsync] {}: {}", $y, $x.ident.to_string());
27            } else {
28                println!("Encountered non-tsync {}: {}", $y, $x.ident.to_string());
29            }
30        }
31
32        if has_tsync_attribute {
33            $z
34        }
35    };
36}
37
38#[derive(Default)]
39pub struct BuildState /*<'a>*/ {
40    pub types: String,
41    pub unprocessed_files: Vec<PathBuf>,
42    // pub ignore_file_config: Option<gitignore::File<'a>>,
43}
44
45/// Settings for the build process
46#[derive(Default)]
47pub struct BuildSettings {
48    pub uses_type_interface: bool,
49    pub enable_const_enums: bool,
50}
51
52// fn should_ignore_file(ignore_file: &gitignore::File, entry: &DirEntry) -> bool {
53//     let path = entry.path();
54
55//     ignore_file.is_excluded(&path).unwrap_or(false)
56// }
57
58fn has_tsync_attribute(attributes: &[syn::Attribute]) -> bool {
59    utils::has_attribute("tsync", attributes)
60}
61
62impl BuildState {
63    fn write_comments(&mut self, comments: &Vec<String>, indentation_amount: i8) {
64        let indentation = utils::build_indentation(indentation_amount);
65        match comments.len() {
66            0 => (),
67            1 => self
68                .types
69                .push_str(&format!("{}/** {} */\n", indentation, &comments[0])),
70            _ => {
71                self.types.push_str(&format!("{}/**\n", indentation));
72                for comment in comments {
73                    self.types
74                        .push_str(&format!("{} * {}\n", indentation, &comment))
75                }
76                self.types.push_str(&format!("{} */\n", indentation))
77            }
78        }
79    }
80}
81
82fn process_rust_item(item: syn::Item, state: &mut BuildState, config: &BuildSettings) {
83    match item {
84        syn::Item::Const(exported_const) => {
85            check_tsync!(exported_const, in: "const", {
86                exported_const.convert_to_ts(state, config);
87            });
88        }
89        syn::Item::Struct(exported_struct) => {
90            check_tsync!(exported_struct, in: "struct", {
91                exported_struct.convert_to_ts(state, config);
92            });
93        }
94        syn::Item::Enum(exported_enum) => {
95            check_tsync!(exported_enum, in: "enum", {
96                exported_enum.convert_to_ts(state, config);
97            });
98        }
99        syn::Item::Type(exported_type) => {
100            check_tsync!(exported_type, in: "type", {
101                exported_type.convert_to_ts(state, config);
102            });
103        }
104        _ => {}
105    }
106}
107
108fn process_rust_file<P: AsRef<Path>>(
109    input_path: P,
110    state: &mut BuildState,
111    config: &BuildSettings,
112) {
113    if *DEBUG.get() {
114        println!("processing rust file: {:?}", input_path.as_ref().to_str());
115    }
116
117    let Ok(src) = std::fs::read_to_string(input_path.as_ref()) else {
118        state
119            .unprocessed_files
120            .push(input_path.as_ref().to_path_buf());
121        return;
122    };
123
124    let Ok(syntax) = syn::parse_file(&src) else {
125        state
126            .unprocessed_files
127            .push(input_path.as_ref().to_path_buf());
128        return;
129    };
130
131    syntax
132        .items
133        .into_iter()
134        .for_each(|item| process_rust_item(item, state, config))
135}
136
137fn check_path<P: AsRef<Path>>(path: P, state: &mut BuildState) -> bool {
138    if !path.as_ref().exists() {
139        if *DEBUG.get() {
140            println!("Path `{:#?}` does not exist", path.as_ref());
141        }
142        state.unprocessed_files.push(path.as_ref().to_path_buf());
143        return false;
144    }
145
146    true
147}
148
149fn check_extension<P: AsRef<Path>>(ext: &OsStr, path: P) -> bool {
150    if !ext.eq_ignore_ascii_case("rs") {
151        if *DEBUG.get() {
152            println!("Encountered non-rust file `{:#?}`", path.as_ref());
153        }
154        return false;
155    }
156
157    true
158}
159
160/// Ensure that the walked entry result is Ok and its path is a file. If not,
161/// return `None`, otherwise return `Some(DirEntry)`.
162fn validate_dir_entry(entry_result: walkdir::Result<DirEntry>, path: &Path) -> Option<DirEntry> {
163    match entry_result {
164        Ok(entry) => {
165            // skip dir files because they're going to be recursively crawled by WalkDir
166            if entry.path().is_dir() {
167                if *DEBUG.get() {
168                    println!("Encountered directory `{}`", path.display());
169                }
170                return None;
171            }
172
173            Some(entry)
174        }
175        Err(e) => {
176            println!(
177                "An error occurred whilst walking directory `{}`...",
178                path.display()
179            );
180            println!("Details: {e:?}");
181            None
182        }
183    }
184}
185
186fn process_dir_entry<P: AsRef<Path>>(path: P, state: &mut BuildState, config: &BuildSettings) {
187    WalkDir::new(path.as_ref())
188        .sort_by_file_name()
189        .into_iter()
190        .filter_map(|res| validate_dir_entry(res, path.as_ref()))
191        .for_each(|entry| {
192            // make sure it is a rust file
193            if entry
194                .path()
195                .extension()
196                .is_some_and(|extension| check_extension(extension, path.as_ref()))
197            {
198                process_rust_file(entry.path(), state, config);
199            }
200        })
201}
202
203pub fn generate_typescript_defs_inner(
204    input: Vec<PathBuf>,
205    uses_type_interface: bool,
206    debug: bool,
207    enable_const_enums: bool,
208) -> BuildState {
209    DEBUG.set(debug);
210    let config = BuildSettings {
211        uses_type_interface,
212        enable_const_enums,
213    };
214
215    let mut state = BuildState::default();
216
217    state
218        .types
219        .push_str("/* This file is generated and managed by tsync */\n");
220
221    input.into_iter().for_each(|path| {
222        if check_path(&path, &mut state) {
223            if path.is_dir() {
224                process_dir_entry(&path, &mut state, &config)
225            } else {
226                process_rust_file(&path, &mut state, &config);
227            }
228        }
229    });
230
231    state
232}
233
234pub fn generate_typescript_defs(
235    input: Vec<PathBuf>,
236    output: PathBuf,
237    debug: bool,
238    enable_const_enums: bool,
239) {
240    DEBUG.set(debug);
241
242    let uses_type_interface = output
243        .to_str()
244        .map(|x| x.ends_with(".d.ts"))
245        .unwrap_or(true);
246
247    let state =
248        generate_typescript_defs_inner(input, uses_type_interface, debug, enable_const_enums);
249
250    if debug {
251        println!("======================================");
252        println!("FINAL FILE:");
253        println!("======================================");
254        println!("{}", state.types);
255        println!("======================================");
256        println!("Note: Nothing is written in debug mode");
257        println!("======================================");
258    } else {
259        // Verify that the output file either doesn't exists or has been generated by tsync.
260        if output.exists() {
261            if !output.is_file() {
262                panic!("Specified output path is a directory but must be a file.")
263            }
264            let original_file = File::open(&output).expect("Couldn't open output file");
265            let mut buffer = BufReader::new(original_file);
266
267            let mut first_line = String::new();
268
269            buffer
270                .read_line(&mut first_line)
271                .expect("Unable to read line");
272
273            if first_line.trim() != "/* This file is generated and managed by tsync */" {
274                panic!("Aborting: specified output file exists but doesn't have \"/* This file is generated and managed by tsync */\" as the first line.")
275            }
276        }
277
278        match std::fs::write(&output, state.types.as_bytes()) {
279            Ok(_) => println!("Successfully generated typescript types, see {:#?}", output),
280            Err(_) => println!("Failed to generate types, an error occurred."),
281        }
282    }
283
284    if !state.unprocessed_files.is_empty() {
285        println!("Could not parse the following files:");
286    }
287
288    for unprocessed_file in state.unprocessed_files {
289        println!("• {:#?}", unprocessed_file);
290    }
291}