Skip to main content

flowjs_rs/
lib.rs

1//! Generate Flow type declarations from Rust types.
2//!
3//! # Usage
4//! ```rust
5//! #[derive(flowjs_rs::Flow)]
6//! struct User {
7//!     user_id: i32,
8//!     first_name: String,
9//!     last_name: String,
10//! }
11//! ```
12//!
13//! When running `cargo test`, the following Flow type will be exported:
14//! ```flow
15//! type User = {|
16//!   +user_id: number,
17//!   +first_name: string,
18//!   +last_name: string,
19//! |};
20//! ```
21
22mod export;
23pub mod flow_type;
24mod impls;
25
26pub use flowjs_rs_macros::Flow;
27
28pub use crate::export::ExportError;
29
30use std::any::TypeId;
31use std::path::{Path, PathBuf};
32
33/// Configuration for Flow type generation.
34#[derive(Debug, Clone)]
35pub struct Config {
36    export_dir: PathBuf,
37    array_tuple_limit: usize,
38    file_extension: String,
39    large_int_type: String,
40}
41
42impl Config {
43    /// Create a new config with default settings.
44    pub fn new() -> Self {
45        Self {
46            export_dir: PathBuf::from("./bindings"),
47            array_tuple_limit: 64,
48            file_extension: "js.flow".to_owned(),
49            large_int_type: "bigint".to_owned(),
50        }
51    }
52
53    /// Read config from environment variables.
54    ///
55    /// | Variable | Default |
56    /// |---|---|
57    /// | `FLOW_RS_EXPORT_DIR` | `./bindings` |
58    /// | `FLOW_RS_FILE_EXTENSION` | `js.flow` |
59    /// | `FLOW_RS_LARGE_INT` | `bigint` |
60    pub fn from_env() -> Self {
61        let mut cfg = Self::new();
62
63        if let Ok(dir) = std::env::var("FLOW_RS_EXPORT_DIR") {
64            cfg = cfg.with_out_dir(dir);
65        }
66
67        if let Ok(ext) = std::env::var("FLOW_RS_FILE_EXTENSION") {
68            cfg = cfg.with_file_extension(ext);
69        }
70
71        if let Ok(ty) = std::env::var("FLOW_RS_LARGE_INT") {
72            cfg = cfg.with_large_int(ty);
73        }
74
75        cfg
76    }
77
78    /// Set the export directory.
79    pub fn with_out_dir(mut self, dir: impl Into<PathBuf>) -> Self {
80        self.export_dir = dir.into();
81        self
82    }
83
84    /// Return the export directory.
85    pub fn out_dir(&self) -> &Path {
86        &self.export_dir
87    }
88
89    /// Set the maximum size of arrays up to which they are treated as Flow tuples.
90    /// Arrays beyond this size will instead result in a `$ReadOnlyArray<T>`.
91    ///
92    /// Default: `64`
93    pub fn with_array_tuple_limit(mut self, limit: usize) -> Self {
94        self.array_tuple_limit = limit;
95        self
96    }
97
98    /// Return the maximum size of arrays treated as tuples.
99    pub fn array_tuple_limit(&self) -> usize {
100        self.array_tuple_limit
101    }
102
103    /// Set the file extension for generated Flow files.
104    ///
105    /// This is determined by your project's JS module system:
106    /// - `"js.flow"` — standard
107    /// - `"cjs.flow"` — CommonJS
108    /// - `"mjs.flow"` — ES modules
109    pub fn with_file_extension(mut self, ext: impl Into<String>) -> Self {
110        self.file_extension = ext.into();
111        self
112    }
113
114    /// Return the file extension for generated files.
115    pub fn file_extension(&self) -> &str {
116        &self.file_extension
117    }
118
119    /// Set the Flow type used for large integers (`i64`, `u64`, `i128`, `u128`).
120    ///
121    /// Default: `"bigint"` (matches ts-rs)
122    pub fn with_large_int(mut self, ty: impl Into<String>) -> Self {
123        self.large_int_type = ty.into();
124        self
125    }
126
127    /// Return the Flow type for large integers.
128    pub fn large_int(&self) -> &str {
129        &self.large_int_type
130    }
131
132    /// Resolve a type's base output path (without extension) into a full path with extension.
133    pub fn resolve_output_path(&self, base: &Path) -> PathBuf {
134        if base.extension().is_some() {
135            base.to_owned()
136        } else {
137            let name = base.to_str().unwrap_or("unknown");
138            PathBuf::from(format!("{name}.{}", self.file_extension()))
139        }
140    }
141}
142
143impl Default for Config {
144    fn default() -> Self {
145        Self::new()
146    }
147}
148
149/// A visitor used to iterate over all dependencies or generics of a type.
150/// When an instance of [`TypeVisitor`] is passed to [`Flow::visit_dependencies`] or
151/// [`Flow::visit_generics`], the [`TypeVisitor::visit`] method will be invoked for every
152/// dependency or generic parameter respectively.
153pub trait TypeVisitor: Sized {
154    fn visit<T: Flow + 'static + ?Sized>(&mut self);
155}
156
157/// A Flow type which is depended upon by other types.
158/// This information is required for generating the correct import statements.
159#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
160pub struct Dependency {
161    /// Type ID of the rust type.
162    pub type_id: TypeId,
163    /// Name of the type in Flow.
164    pub flow_name: String,
165    /// Path to where the type would be exported. By default, a filename is derived from the
166    /// type name, which can be customized with `#[flow(export_to = "..")]`.
167    /// This path does _not_ include a base directory.
168    pub output_path: PathBuf,
169}
170
171impl Dependency {
172    /// Construct a [`Dependency`] from the given type `T`.
173    /// If `T` is not exportable (meaning `T::output_path()` returns `None`), this function
174    /// will return `None`.
175    pub fn from_ty<T: Flow + 'static + ?Sized>(cfg: &Config) -> Option<Self> {
176        let output_path = <T as crate::Flow>::output_path()?;
177        Some(Dependency {
178            type_id: TypeId::of::<T>(),
179            flow_name: <T as crate::Flow>::ident(cfg),
180            output_path,
181        })
182    }
183}
184
185/// The core trait. Derive it on your types to generate Flow declarations.
186///
187/// Mirrors the ts-rs `TS` trait interface.
188pub trait Flow {
189    /// If this type does not have generic parameters, then `WithoutGenerics` should be `Self`.
190    /// If the type does have generic parameters, then all generic parameters must be replaced
191    /// with a dummy type, e.g `flowjs_rs::Dummy` or `()`.
192    /// The only requirement for these dummy types is that `output_path()` must return `None`.
193    type WithoutGenerics: Flow + ?Sized;
194
195    /// If the implementing type is `std::option::Option<T>`, then this associated type is set
196    /// to `T`. All other implementations of `Flow` should set this type to `Self` instead.
197    type OptionInnerType: ?Sized;
198
199    #[doc(hidden)]
200    const IS_OPTION: bool = false;
201
202    /// Whether this is an enum type.
203    const IS_ENUM: bool = false;
204
205    /// JSDoc/Flow comment to describe this type -- when `Flow` is derived, docs are
206    /// automatically read from your doc comments or `#[doc = ".."]` attributes.
207    fn docs() -> Option<String> {
208        None
209    }
210
211    /// Identifier of this type, excluding generic parameters.
212    fn ident(cfg: &Config) -> String {
213        let name = <Self as crate::Flow>::name(cfg);
214        match name.find('<') {
215            Some(i) => name[..i].to_owned(),
216            None => name,
217        }
218    }
219
220    /// Declaration of this type, e.g. `type User = {| +user_id: number |};`.
221    /// This function will panic if the type has no declaration.
222    ///
223    /// If this type is generic, then all provided generic parameters will be swapped for
224    /// placeholders, resulting in a generic Flow definition.
225    fn decl(cfg: &Config) -> String {
226        panic!("{} cannot be declared", Self::name(cfg))
227    }
228
229    /// Declaration of this type using the supplied generic arguments.
230    /// The resulting Flow definition will not be generic. For that, see `Flow::decl()`.
231    /// If this type is not generic, then this function is equivalent to `Flow::decl()`.
232    fn decl_concrete(cfg: &Config) -> String {
233        panic!("{} cannot be declared", Self::name(cfg))
234    }
235
236    /// Flow type name, including generic parameters.
237    fn name(cfg: &Config) -> String;
238
239    /// Inline Flow type definition (the right-hand side of `type X = ...`).
240    fn inline(cfg: &Config) -> String;
241
242    /// Flatten a type declaration.
243    /// This function will panic if the type cannot be flattened.
244    fn inline_flattened(cfg: &Config) -> String {
245        panic!("{} cannot be flattened", Self::name(cfg))
246    }
247
248    /// Iterate over all dependencies of this type.
249    fn visit_dependencies(_: &mut impl TypeVisitor)
250    where
251        Self: 'static,
252    {
253    }
254
255    /// Iterate over all type parameters of this type.
256    fn visit_generics(_: &mut impl TypeVisitor)
257    where
258        Self: 'static,
259    {
260    }
261
262    /// Resolve all dependencies of this type recursively.
263    fn dependencies(cfg: &Config) -> Vec<Dependency>
264    where
265        Self: 'static,
266    {
267        struct Visit<'a>(&'a Config, &'a mut Vec<Dependency>);
268        impl TypeVisitor for Visit<'_> {
269            fn visit<T: Flow + 'static + ?Sized>(&mut self) {
270                let Visit(cfg, deps) = self;
271                if let Some(dep) = Dependency::from_ty::<T>(cfg) {
272                    deps.push(dep);
273                }
274            }
275        }
276
277        let mut deps: Vec<Dependency> = vec![];
278        Self::visit_dependencies(&mut Visit(cfg, &mut deps));
279        deps
280    }
281
282    /// Output file path relative to the export directory.
283    fn output_path() -> Option<PathBuf> {
284        None
285    }
286
287    /// Export this type to disk.
288    fn export(cfg: &Config) -> Result<(), ExportError>
289    where
290        Self: 'static,
291    {
292        let base = Self::output_path()
293            .ok_or(ExportError::CannotBeExported(std::any::type_name::<Self>()))?;
294        let relative = cfg.resolve_output_path(&base);
295        let path = cfg.export_dir.join(relative);
296        export::export_to::<Self>(cfg, &path)
297    }
298
299    /// Export this type to disk, together with all of its dependencies.
300    fn export_all(cfg: &Config) -> Result<(), ExportError>
301    where
302        Self: 'static,
303    {
304        export::export_all_into::<Self>(cfg)
305    }
306
307    /// Render this type as a string, returning the full file content.
308    fn export_to_string(cfg: &Config) -> Result<String, ExportError>
309    where
310        Self: 'static,
311    {
312        export::export_to_string::<Self>(cfg)
313    }
314}
315
316/// Dummy type used as a placeholder for generic parameters during codegen.
317pub struct Dummy;
318
319impl Flow for Dummy {
320    type WithoutGenerics = Self;
321    type OptionInnerType = Self;
322
323    fn name(_: &Config) -> String {
324        flow_type::ANY.to_owned()
325    }
326    fn inline(_: &Config) -> String {
327        flow_type::ANY.to_owned()
328    }
329}