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}
39
40impl Config {
41    /// Create a new config with default settings.
42    pub fn new() -> Self {
43        Self {
44            export_dir: PathBuf::from("./bindings"),
45            array_tuple_limit: 64,
46        }
47    }
48
49    /// Read config from environment variables.
50    ///
51    /// | Variable | Default |
52    /// |---|---|
53    /// | `FLOW_RS_EXPORT_DIR` | `./bindings` |
54    pub fn from_env() -> Self {
55        let mut cfg = Self::new();
56
57        if let Ok(dir) = std::env::var("FLOW_RS_EXPORT_DIR") {
58            cfg = cfg.with_out_dir(dir);
59        }
60
61        cfg
62    }
63
64    /// Set the export directory.
65    pub fn with_out_dir(mut self, dir: impl Into<PathBuf>) -> Self {
66        self.export_dir = dir.into();
67        self
68    }
69
70    /// Return the export directory.
71    pub fn out_dir(&self) -> &Path {
72        &self.export_dir
73    }
74
75    /// Set the maximum size of arrays up to which they are treated as Flow tuples.
76    /// Arrays beyond this size will instead result in a `$ReadOnlyArray<T>`.
77    ///
78    /// Default: `64`
79    pub fn with_array_tuple_limit(mut self, limit: usize) -> Self {
80        self.array_tuple_limit = limit;
81        self
82    }
83
84    /// Return the maximum size of arrays treated as tuples.
85    pub fn array_tuple_limit(&self) -> usize {
86        self.array_tuple_limit
87    }
88}
89
90impl Default for Config {
91    fn default() -> Self {
92        Self::new()
93    }
94}
95
96/// A visitor used to iterate over all dependencies or generics of a type.
97/// When an instance of [`TypeVisitor`] is passed to [`Flow::visit_dependencies`] or
98/// [`Flow::visit_generics`], the [`TypeVisitor::visit`] method will be invoked for every
99/// dependency or generic parameter respectively.
100pub trait TypeVisitor: Sized {
101    fn visit<T: Flow + 'static + ?Sized>(&mut self);
102}
103
104/// A Flow type which is depended upon by other types.
105/// This information is required for generating the correct import statements.
106#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
107pub struct Dependency {
108    /// Type ID of the rust type.
109    pub type_id: TypeId,
110    /// Name of the type in Flow.
111    pub flow_name: String,
112    /// Path to where the type would be exported. By default, a filename is derived from the
113    /// type name, which can be customized with `#[flow(export_to = "..")]`.
114    /// This path does _not_ include a base directory.
115    pub output_path: PathBuf,
116}
117
118impl Dependency {
119    /// Construct a [`Dependency`] from the given type `T`.
120    /// If `T` is not exportable (meaning `T::output_path()` returns `None`), this function
121    /// will return `None`.
122    pub fn from_ty<T: Flow + 'static + ?Sized>(cfg: &Config) -> Option<Self> {
123        let output_path = <T as crate::Flow>::output_path()?;
124        Some(Dependency {
125            type_id: TypeId::of::<T>(),
126            flow_name: <T as crate::Flow>::ident(cfg),
127            output_path,
128        })
129    }
130}
131
132/// The core trait. Derive it on your types to generate Flow declarations.
133///
134/// Mirrors the ts-rs `TS` trait interface.
135pub trait Flow {
136    /// If this type does not have generic parameters, then `WithoutGenerics` should be `Self`.
137    /// If the type does have generic parameters, then all generic parameters must be replaced
138    /// with a dummy type, e.g `flowjs_rs::Dummy` or `()`.
139    /// The only requirement for these dummy types is that `output_path()` must return `None`.
140    type WithoutGenerics: Flow + ?Sized;
141
142    /// If the implementing type is `std::option::Option<T>`, then this associated type is set
143    /// to `T`. All other implementations of `Flow` should set this type to `Self` instead.
144    type OptionInnerType: ?Sized;
145
146    #[doc(hidden)]
147    const IS_OPTION: bool = false;
148
149    /// Whether this is an enum type.
150    const IS_ENUM: bool = false;
151
152    /// JSDoc/Flow comment to describe this type -- when `Flow` is derived, docs are
153    /// automatically read from your doc comments or `#[doc = ".."]` attributes.
154    fn docs() -> Option<String> {
155        None
156    }
157
158    /// Identifier of this type, excluding generic parameters.
159    fn ident(cfg: &Config) -> String {
160        let name = <Self as crate::Flow>::name(cfg);
161        match name.find('<') {
162            Some(i) => name[..i].to_owned(),
163            None => name,
164        }
165    }
166
167    /// Declaration of this type, e.g. `type User = {| +user_id: number |};`.
168    /// This function will panic if the type has no declaration.
169    ///
170    /// If this type is generic, then all provided generic parameters will be swapped for
171    /// placeholders, resulting in a generic Flow definition.
172    fn decl(cfg: &Config) -> String {
173        panic!("{} cannot be declared", Self::name(cfg))
174    }
175
176    /// Declaration of this type using the supplied generic arguments.
177    /// The resulting Flow definition will not be generic. For that, see `Flow::decl()`.
178    /// If this type is not generic, then this function is equivalent to `Flow::decl()`.
179    fn decl_concrete(cfg: &Config) -> String {
180        panic!("{} cannot be declared", Self::name(cfg))
181    }
182
183    /// Flow type name, including generic parameters.
184    fn name(cfg: &Config) -> String;
185
186    /// Inline Flow type definition (the right-hand side of `type X = ...`).
187    fn inline(cfg: &Config) -> String;
188
189    /// Flatten a type declaration.
190    /// This function will panic if the type cannot be flattened.
191    fn inline_flattened(cfg: &Config) -> String {
192        panic!("{} cannot be flattened", Self::name(cfg))
193    }
194
195    /// Iterate over all dependencies of this type.
196    fn visit_dependencies(_: &mut impl TypeVisitor)
197    where
198        Self: 'static,
199    {
200    }
201
202    /// Iterate over all type parameters of this type.
203    fn visit_generics(_: &mut impl TypeVisitor)
204    where
205        Self: 'static,
206    {
207    }
208
209    /// Resolve all dependencies of this type recursively.
210    fn dependencies(cfg: &Config) -> Vec<Dependency>
211    where
212        Self: 'static,
213    {
214        struct Visit<'a>(&'a Config, &'a mut Vec<Dependency>);
215        impl TypeVisitor for Visit<'_> {
216            fn visit<T: Flow + 'static + ?Sized>(&mut self) {
217                let Visit(cfg, deps) = self;
218                if let Some(dep) = Dependency::from_ty::<T>(cfg) {
219                    deps.push(dep);
220                }
221            }
222        }
223
224        let mut deps: Vec<Dependency> = vec![];
225        Self::visit_dependencies(&mut Visit(cfg, &mut deps));
226        deps
227    }
228
229    /// Output file path relative to the export directory.
230    fn output_path() -> Option<PathBuf> {
231        None
232    }
233
234    /// Export this type to disk.
235    fn export(cfg: &Config) -> Result<(), ExportError>
236    where
237        Self: 'static,
238    {
239        let relative = Self::output_path()
240            .ok_or(ExportError::CannotBeExported(std::any::type_name::<Self>()))?;
241        let path = cfg.export_dir.join(relative);
242        export::export_to::<Self>(cfg, &path)
243    }
244
245    /// Export this type to disk, together with all of its dependencies.
246    fn export_all(cfg: &Config) -> Result<(), ExportError>
247    where
248        Self: 'static,
249    {
250        export::export_all_into::<Self>(cfg)
251    }
252
253    /// Render this type as a string, returning the full file content.
254    fn export_to_string(cfg: &Config) -> Result<String, ExportError>
255    where
256        Self: 'static,
257    {
258        export::export_to_string::<Self>(cfg)
259    }
260}
261
262/// Dummy type used as a placeholder for generic parameters during codegen.
263pub struct Dummy;
264
265impl Flow for Dummy {
266    type WithoutGenerics = Self;
267    type OptionInnerType = Self;
268
269    fn name(_: &Config) -> String {
270        flow_type::ANY.to_owned()
271    }
272    fn inline(_: &Config) -> String {
273        flow_type::ANY.to_owned()
274    }
275}