1use std::any::TypeId;
14use std::collections::{HashMap, HashSet};
15use std::path::{Component, Path, PathBuf};
16use std::sync::{Mutex, OnceLock};
17
18#[derive(Debug, thiserror::Error)]
20pub enum ExportError {
21 #[error("type `{0}` cannot be exported")]
22 CannotBeExported(&'static str),
23 #[error(transparent)]
24 Io(#[from] std::io::Error),
25 #[error(transparent)]
26 Fmt(#[from] std::fmt::Error),
27}
28
29pub trait TypeVisitor: Sized {
31 fn visit<T: ExportableType + 'static + ?Sized>(&mut self);
32}
33
34pub trait ExportableType {
39 fn type_name(cfg: &ExportConfig) -> String;
41
42 fn type_ident(cfg: &ExportConfig) -> String {
44 let name = Self::type_name(cfg);
45 match name.find('<') {
46 Some(i) => name[..i].to_owned(),
47 None => name,
48 }
49 }
50
51 fn output_path() -> Option<PathBuf> {
53 None
54 }
55
56 fn visit_dependencies(_: &mut impl TypeVisitor)
58 where
59 Self: 'static,
60 {
61 }
62
63 fn visit_generics(_: &mut impl TypeVisitor)
65 where
66 Self: 'static,
67 {
68 }
69}
70
71#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
73pub struct Dependency {
74 pub type_id: TypeId,
75 pub name: String,
76 pub output_path: PathBuf,
77}
78
79#[derive(Debug, Clone)]
81pub struct ExportConfig {
82 pub export_dir: PathBuf,
83 pub file_extension: String,
84 pub array_tuple_limit: usize,
85 pub large_int_type: String,
86}
87
88impl Default for ExportConfig {
89 fn default() -> Self {
90 Self {
91 export_dir: PathBuf::from("./bindings"),
92 file_extension: "js.flow".to_owned(),
93 array_tuple_limit: 64,
94 large_int_type: "bigint".to_owned(),
95 }
96 }
97}
98
99impl ExportConfig {
100 pub fn resolve_path(&self, base: &Path) -> PathBuf {
102 if base.extension().is_some() {
103 base.to_owned()
104 } else {
105 let name = base.to_str().unwrap_or("unknown");
106 PathBuf::from(format!("{name}.{}", self.file_extension))
107 }
108 }
109}
110
111pub fn diff_paths(target: &Path, base: &Path) -> PathBuf {
113 let target_components: Vec<Component<'_>> = target.components().collect();
114 let base_components: Vec<Component<'_>> = base.components().collect();
115
116 let common = target_components
117 .iter()
118 .zip(base_components.iter())
119 .take_while(|(a, b)| a == b)
120 .count();
121
122 let mut result = PathBuf::new();
123 for _ in common..base_components.len() {
124 result.push("..");
125 }
126 for component in &target_components[common..] {
127 result.push(component);
128 }
129
130 if result.as_os_str().is_empty() {
131 PathBuf::from(".")
132 } else {
133 result
134 }
135}
136
137pub fn normalize_separators(s: &str) -> String {
139 s.replace('\\', "/")
140}
141
142pub fn relative_import_path(from: &Path, to: &Path) -> String {
144 let from_dir = from.parent().unwrap_or(Path::new(""));
145 let rel = diff_paths(to, from_dir);
146 let s = normalize_separators(rel.to_str().unwrap_or("./unknown"));
147
148 if s.starts_with("../") || s.starts_with("./") {
149 s
150 } else {
151 format!("./{s}")
152 }
153}
154
155pub fn file_lock(path: &Path) -> &'static Mutex<()> {
157 static LOCKS: OnceLock<Mutex<HashMap<PathBuf, &'static Mutex<()>>>> = OnceLock::new();
158 let locks = LOCKS.get_or_init(|| Mutex::new(HashMap::new()));
159 let mut map = locks.lock().unwrap_or_else(|e| e.into_inner());
160 let canonical = path.to_path_buf();
161 map.entry(canonical)
162 .or_insert_with(|| Box::leak(Box::new(Mutex::new(()))))
163}
164
165pub fn write_with_lock(
170 path: &Path,
171 marker: &str,
172 content_fn: impl FnOnce() -> String,
173 append_fn: Option<impl FnOnce(&str) -> String>,
174) -> Result<(), ExportError> {
175 if let Some(parent) = path.parent() {
176 std::fs::create_dir_all(parent)?;
177 }
178
179 let _guard = file_lock(path)
180 .lock()
181 .unwrap_or_else(|e| e.into_inner());
182
183 if path.exists() {
184 let existing = std::fs::read_to_string(path)?;
185 if existing.contains(marker) {
186 return Ok(());
187 }
188 if let Some(append) = append_fn {
189 std::fs::write(path, append(&existing))?;
190 }
191 } else {
192 std::fs::write(path, content_fn())?;
193 }
194
195 Ok(())
196}
197
198pub fn export_recursive<T: ExportableType + 'static + ?Sized>(
200 cfg: &ExportConfig,
201 seen: &mut HashSet<TypeId>,
202 export_one: &dyn Fn(&ExportConfig, &Path) -> Result<(), ExportError>,
203) -> Result<(), ExportError> {
204 if !seen.insert(TypeId::of::<T>()) {
205 return Ok(());
206 }
207
208 struct Visit<'a> {
209 cfg: &'a ExportConfig,
210 seen: &'a mut HashSet<TypeId>,
211 export_one: &'a dyn Fn(&ExportConfig, &Path) -> Result<(), ExportError>,
212 error: Option<ExportError>,
213 }
214
215 impl TypeVisitor for Visit<'_> {
216 fn visit<U: ExportableType + 'static + ?Sized>(&mut self) {
217 if self.error.is_some() || U::output_path().is_none() {
218 return;
219 }
220 self.error =
221 export_recursive::<U>(self.cfg, self.seen, self.export_one).err();
222 }
223 }
224
225 let mut visitor = Visit {
226 cfg,
227 seen,
228 export_one,
229 error: None,
230 };
231 T::visit_dependencies(&mut visitor);
232
233 if let Some(e) = visitor.error {
234 return Err(e);
235 }
236
237 let base = T::output_path()
238 .ok_or(ExportError::CannotBeExported(std::any::type_name::<T>()))?;
239 let relative = cfg.resolve_path(&base);
240 let path = cfg.export_dir.join(relative);
241 export_one(cfg, &path)
242}