Skip to main content

type_export/
lib.rs

1//! Shared export infrastructure for type declaration generators.
2//!
3//! Provides the core traits and utilities that both flowjs-rs and ts-rs
4//! (and any future type-declaration-from-Rust project) need:
5//!
6//! - `TypeVisitor` trait for dependency graph walking
7//! - `Dependency` struct for import generation
8//! - `ExportConfig` for configuring output paths and extensions
9//! - `ExportError` for error handling
10//! - File export with thread-safe locking and idempotent writes
11//! - Relative import path calculation
12
13use std::any::TypeId;
14use std::collections::{HashMap, HashSet};
15use std::path::{Component, Path, PathBuf};
16use std::sync::{Mutex, OnceLock};
17
18/// Export error.
19#[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
29/// A visitor used to iterate over all dependencies or generics of a type.
30pub trait TypeVisitor: Sized {
31    fn visit<T: ExportableType + 'static + ?Sized>(&mut self);
32}
33
34/// Core trait that all exportable types implement.
35///
36/// This is the type-system-agnostic base. Language-specific traits
37/// (Flow's `Flow`, ts-rs's `TS`) extend this with syntax-specific methods.
38pub trait ExportableType {
39    /// Type name (may include generic parameters).
40    fn type_name(cfg: &ExportConfig) -> String;
41
42    /// Identifier without generic parameters.
43    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    /// Output file path (relative to export dir), without extension.
52    fn output_path() -> Option<PathBuf> {
53        None
54    }
55
56    /// Visit all direct dependencies.
57    fn visit_dependencies(_: &mut impl TypeVisitor)
58    where
59        Self: 'static,
60    {
61    }
62
63    /// Visit all generic type parameters.
64    fn visit_generics(_: &mut impl TypeVisitor)
65    where
66        Self: 'static,
67    {
68    }
69}
70
71/// A type dependency for import generation.
72#[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/// Configuration for type export.
80#[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    /// Resolve a base path (without extension) to a full path with extension.
101    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
111/// Compute a relative path from `base` to `target`.
112pub 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
137/// Normalize Windows backslashes to forward slashes for JS import paths.
138pub fn normalize_separators(s: &str) -> String {
139    s.replace('\\', "/")
140}
141
142/// Compute a relative import path from one file to another.
143pub 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
155/// Per-file mutex for thread-safe exports.
156pub 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
165/// Write content to a file, with idempotent marker checking.
166///
167/// If the file already contains a marker string, the write is skipped.
168/// Thread-safe via per-file locking.
169pub 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
198/// Recursively export a type and all its dependencies.
199pub 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}