postcard_bindgen_core/code_gen/js/
mod.rs

1mod des;
2mod general;
3mod generateable;
4mod ser;
5mod type_checks;
6
7use core::borrow::Borrow;
8
9use des::{gen_des_functions, gen_deserialize_func, gen_deserializer_code};
10use genco::{
11    prelude::js::JavaScript,
12    quote_in,
13    tokens::{quoted, FormatInto},
14};
15use general::gen_util;
16use generateable::gen_ts_typings;
17use ser::{gen_ser_functions, gen_serialize_func, gen_serializer_code};
18use type_checks::gen_type_checks;
19
20use crate::{registry::ContainerCollection, ExportFile, Exports};
21
22use super::{export_registry::ExportMode, utils::TokensIterExt};
23
24const JS_ENUM_VARIANT_KEY: &str = "tag";
25const JS_ENUM_VARIANT_VALUE: &str = "value";
26const JS_OBJECT_VARIABLE: &str = "v";
27const JS_LOGIC_AND: &str = "&&";
28const JS_LOGIC_OR: &str = "||";
29
30type Tokens = genco::Tokens<JavaScript>;
31
32type VariablePath = super::variable_path::VariablePath<JavaScript>;
33type VariableAccess = super::variable_path::VariableAccess;
34type FieldAccessor<'a> = super::field_accessor::FieldAccessor<'a>;
35type AvailableCheck = super::available_check::AvailableCheck<JavaScript>;
36type Function = super::function::Function<JavaScript>;
37type FunctionArg = super::function::FunctionArg<JavaScript>;
38type ExportRegistry = super::export_registry::ExportRegistry<JavaScript>;
39type Case = super::switch_case::Case<JavaScript>;
40type DefaultCase = super::switch_case::DefaultCase<JavaScript>;
41type SwitchCase = super::switch_case::SwitchCase<JavaScript>;
42
43/// Settings for bindings generation.
44///
45/// This enables the possibility to enable or disable serialization, deserialization, runtime type checks
46/// or type script types.
47/// Less code will be generated if an option is off.
48///
49/// By default, only deserialization is enabled. Serialization can be enabled by using [`GenerationSettings::serialization()`].
50/// Deserialization can be disabled with [`GenerationSettings::deserialization()`].
51/// To enable all at once use [`GenerationSettings::enable_all()`].
52#[derive(Debug)]
53pub struct GenerationSettings {
54    ser: bool,
55    des: bool,
56    runtime_type_checks: bool,
57    type_script_types: bool,
58    module_structure: bool,
59    esm_module: bool,
60}
61
62impl GenerationSettings {
63    /// Constructs [`GenerationSettings`] and enables all options at once.
64    pub fn enable_all() -> Self {
65        Self {
66            ser: true,
67            des: true,
68            runtime_type_checks: true,
69            type_script_types: true,
70            module_structure: true,
71            esm_module: true,
72        }
73    }
74
75    /// Enabling or disabling of serialization code generation.
76    pub fn serialization(mut self, enabled: bool) -> Self {
77        self.ser = enabled;
78        self
79    }
80
81    /// Enabling or disabling of deserialization code generation.
82    pub fn deserialization(mut self, enabled: bool) -> Self {
83        self.des = enabled;
84        self
85    }
86
87    /// Enabling or disabling of typescript types code generation.
88    ///
89    /// When enabling this, runtime type checks could be disabled with ['GenerationSettings::runtime_type_checks()']
90    /// because the static type checks should enforce correct types. Additionally serialization should be faster
91    /// without runtime type checks.
92    pub fn type_script_types(mut self, enabled: bool) -> Self {
93        self.type_script_types = enabled;
94        self
95    }
96
97    /// Enabling or disabling of runtime type checks code generation.
98    ///
99    /// Disabling this should lead to a speed increase at serialization.
100    pub fn runtime_type_checks(mut self, enabled: bool) -> Self {
101        self.runtime_type_checks = enabled;
102        self
103    }
104
105    /// Enabling or disabling of module structure code generation.
106    ///
107    /// Enabling this will generate the types in typescript in the same module structure
108    /// as in rust. Root level types will be in the root of the generated
109    /// package. Types nested in modules will be in namespaces
110    /// (e.g. <mod_name>.<type_name>). This avoids name clashes.
111    ///
112    /// Disabling this will generate all types in the root module.
113    pub fn module_structure(mut self, enabled: bool) -> Self {
114        self.module_structure = enabled;
115        self
116    }
117
118    /// Enabling or disabling ESM (as opposed to cjs) output
119    ///
120    /// Enabling will change the way the `serialize` and `desrialize`
121    /// functions are exported to bring them in line with ESM standards/importers.
122    /// The package.json file also gets `"type": "module"` added,
123    /// so package managers/bundlers importing it know it's ESM.
124    ///
125    ///
126    /// Disabling this will use the default `module.exports`-style export (cjs)
127    pub fn esm_module(mut self, enabled: bool) -> Self {
128        self.esm_module = enabled;
129        self
130    }
131}
132
133impl Default for GenerationSettings {
134    fn default() -> Self {
135        Self {
136            ser: false,
137            des: true,
138            runtime_type_checks: false,
139            type_script_types: false,
140            module_structure: true,
141            esm_module: false,
142        }
143    }
144}
145
146/// Metadata for JS export
147///
148/// Contains information about the exported JS package needed to
149/// complete the full npm_package (e.g. if it's an ESM module or not)
150pub struct ExportMeta {
151    pub esm_module: bool,
152}
153
154pub fn generate(
155    mut containers: ContainerCollection,
156    gen_settings: impl Borrow<GenerationSettings>,
157) -> (Exports<JavaScript>, ExportMeta) {
158    let gen_settings = gen_settings.borrow();
159
160    if !gen_settings.module_structure {
161        containers.flatten();
162    }
163
164    let export_mode = if gen_settings.esm_module {
165        ExportMode::Esm
166    } else {
167        ExportMode::Cjs
168    };
169
170    let mut export_files = Vec::new();
171
172    export_files.push(ExportFile {
173        content_type: "util".to_owned(),
174        content: gen_util(),
175    });
176
177    if gen_settings.ser {
178        export_files.push(ExportFile {
179            content_type: "serializer".to_owned(),
180            content: gen_serializer_code(),
181        });
182
183        let mut tokens = Tokens::new();
184
185        tokens.append(gen_ser_functions(containers.all_containers()));
186        tokens.line();
187
188        let mut export_registry = ExportRegistry::new(export_mode.clone());
189
190        tokens.append(gen_serialize_func(
191            containers.all_containers(),
192            gen_settings.runtime_type_checks,
193            &mut export_registry,
194        ));
195
196        tokens.line();
197        tokens.append(export_registry);
198
199        export_files.push(ExportFile {
200            content_type: "ser".to_owned(),
201            content: tokens,
202        });
203    }
204
205    if gen_settings.des {
206        export_files.push(ExportFile {
207            content_type: "deserializer".to_owned(),
208            content: gen_deserializer_code(),
209        });
210
211        let mut tokens = Tokens::new();
212
213        tokens.append(gen_des_functions(containers.all_containers()));
214        tokens.line();
215
216        let mut export_registry = ExportRegistry::new(export_mode);
217
218        tokens.append(gen_deserialize_func(
219            containers.all_containers(),
220            &mut export_registry,
221        ));
222        tokens.line();
223
224        tokens.append(export_registry);
225
226        export_files.push(ExportFile {
227            content_type: "des".to_owned(),
228            content: tokens,
229        });
230    }
231
232    if gen_settings.runtime_type_checks {
233        export_files.push(ExportFile {
234            content_type: "runtime_checks".to_owned(),
235            content: gen_type_checks(containers.all_containers()),
236        });
237    }
238
239    if gen_settings.type_script_types {
240        let ts = gen_ts_typings(&containers, gen_settings);
241        export_files.push(ExportFile {
242            content_type: "ts".to_owned(),
243            content: ts,
244        });
245    }
246
247    // Create metadata about export
248    let export_metadata = ExportMeta {
249        esm_module: gen_settings.esm_module,
250    };
251
252    (
253        Exports {
254            files: export_files,
255        },
256        export_metadata,
257    )
258}
259
260impl<I, F> TokensIterExt<JavaScript, F> for I
261where
262    I: Iterator<Item = F>,
263    F: FormatInto<JavaScript>,
264{
265    const LOGICAL_AND: &'static str = JS_LOGIC_AND;
266    const LOGICAL_OR: &'static str = JS_LOGIC_OR;
267}
268
269impl FormatInto<JavaScript> for FieldAccessor<'_> {
270    fn format_into(self, tokens: &mut Tokens) {
271        quote_in! { *tokens =>
272            $(match self {
273                Self::Array | Self::None => (),
274                Self::Object(n) => $n:$[' '],
275            })
276        }
277    }
278}
279
280impl FormatInto<JavaScript> for VariablePath {
281    fn format_into(self, tokens: &mut Tokens) {
282        quote_in! { *tokens =>
283            $(self.start_variable)
284        }
285        self.parts
286            .into_iter()
287            .for_each(|part| part.format_into(tokens))
288    }
289}
290
291impl Default for VariablePath {
292    fn default() -> Self {
293        Self::new(JS_OBJECT_VARIABLE.to_owned())
294    }
295}
296
297impl FormatInto<JavaScript> for VariableAccess {
298    fn format_into(self, tokens: &mut Tokens) {
299        quote_in! { *tokens =>
300            $(match self {
301                Self::Indexed(index) => [$index],
302                Self::Field(name) => .$name,
303            })
304        }
305    }
306}
307
308impl FormatInto<JavaScript> for AvailableCheck {
309    fn format_into(self, tokens: &mut Tokens) {
310        quote_in! { *tokens =>
311            $(match self {
312                AvailableCheck::Object(path, name) => $(quoted(name)) in $path,
313                AvailableCheck::None => ()
314            })
315        }
316    }
317}
318
319impl FormatInto<JavaScript> for FunctionArg {
320    fn format_into(self, tokens: &mut Tokens) {
321        quote_in! { *tokens =>
322            $(self.name)
323        }
324    }
325}
326
327impl FormatInto<JavaScript> for Function {
328    fn format_into(self, tokens: &mut Tokens) {
329        let doc_string = self.doc_string.map(|doc| {
330            let mut tokens = Tokens::new();
331            tokens.append("/**\n");
332            tokens.append(
333                doc.lines()
334                    .map(|line| format!(" * {}", line.trim()))
335                    .collect::<Vec<_>>()
336                    .join("\n"),
337            );
338            tokens.push();
339            tokens.append(" */");
340            tokens
341        });
342        quote_in! { *tokens =>
343            $(doc_string)
344            function $(self.name)($(for arg in self.args join (, ) => $arg)) {
345                $(self.body)
346            }
347        }
348    }
349}
350
351impl FormatInto<JavaScript> for ExportRegistry {
352    fn format_into(self, tokens: &mut Tokens) {
353        match self.export_mode {
354            ExportMode::Cjs => {
355                quote_in! { *tokens =>
356                    $(for export in self.exports join () => exports.$(&export) = $export)
357                }
358            }
359            ExportMode::Esm => {
360                quote_in! { *tokens =>
361                    export {
362                        $(for export in self.exports join (,) => $export)
363                    };
364                }
365            }
366        }
367    }
368}
369
370impl FormatInto<JavaScript> for Case {
371    fn format_into(self, tokens: &mut Tokens) {
372        quote_in! {*tokens =>
373            case $(self.case):
374                $(self.body)
375                $(if self.break_after { break; })
376        }
377    }
378}
379
380impl FormatInto<JavaScript> for DefaultCase {
381    fn format_into(self, tokens: &mut Tokens) {
382        quote_in! { *tokens =>
383            default:
384                $(self.body)
385                $(if self.break_after { break; })
386        }
387    }
388}
389
390impl FormatInto<JavaScript> for SwitchCase {
391    fn format_into(self, tokens: &mut Tokens) {
392        quote_in! { *tokens =>
393            switch ($(self.switch_arg)) {
394            $(for case in self.cases => $case)
395            $(if let Some(default_case) = self.default_case { $default_case })
396            }
397        }
398    }
399}