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}