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
12pub use tsync_macro::tsync;
14
15use crate::to_typescript::ToTypescript;
16
17pub(crate) static DEBUG: InitCell<bool> = InitCell::new();
18
19macro_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 {
40 pub types: String,
41 pub unprocessed_files: Vec<PathBuf>,
42 }
44
45#[derive(Default)]
47pub struct BuildSettings {
48 pub uses_type_interface: bool,
49 pub enable_const_enums: bool,
50}
51
52fn 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
160fn validate_dir_entry(entry_result: walkdir::Result<DirEntry>, path: &Path) -> Option<DirEntry> {
163 match entry_result {
164 Ok(entry) => {
165 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 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 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}