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}