serialize_to_javascript/
lib.rs

1//! Serialize [`serde::Serialize`] values to JavaScript using [`serde_json`].
2//!
3//! # Serialization
4//!
5//! The [`Serialized`] item can help you create a valid JavaScript value out of a
6//! [`serde_json::value::RawValue`], along with some helpful options. It implements [`fmt::Display`]
7//! for direct use, but you can also manually remove it from the [new-type] with
8//! [`Serialized::into_string()`].
9//!
10//! ```rust
11//! use serialize_to_javascript::{Options, Serialized};
12//!
13//! fn main() -> serialize_to_javascript::Result<()> {
14//!     let raw_value = serde_json::value::to_raw_value("foo'bar")?;
15//!     let serialized = Serialized::new(&raw_value, &Options::default());
16//!     assert_eq!(serialized.into_string(), "JSON.parse('\"foo\\'bar\"')");
17//!     Ok(())
18//! }
19//! ```
20//!
21//! # Templating
22//!
23//! Because of the very common case of wanting to include your JavaScript values into existing
24//! JavaScript code, this crate also provides some templating features. [`Template`] helps you map
25//! struct fields into template values, while [`DefaultTemplate`] lets you attach it to a specific
26//! JavaScript file. See their documentation for more details on how to create and use them.
27//!
28//! Templated names that are replaced inside templates are `__TEMPLATE_my_field__` where `my_field`
29//! is a field on a struct implementing [`Template`]. Raw (`#[raw]` field annotation) value template
30//! names use `__RAW_my_field__`. Raw values are inserted directly **without ANY** serialization
31//! whatsoever, so being extra careful where it is used is highly recommended.
32//!
33//! ```rust
34//! use serialize_to_javascript::{default_template, DefaultTemplate, Options, Serialized, Template};
35//!
36//! #[derive(Template)]
37//! #[default_template("../tests/keygen.js")]
38//! struct Keygen<'a> {
39//!     key: &'a str,
40//!     length: usize,
41//!
42//!     #[raw]
43//!     optional_script: &'static str,
44//! }
45//!
46//! fn main() -> serialize_to_javascript::Result<()> {
47//!     let keygen = Keygen {
48//!         key: "asdf",
49//!         length: 4,
50//!         optional_script: "console.log('hello, from my optional script')",
51//!     };
52//!
53//!     let output: Serialized = keygen.render_default(&Options::default())?;
54//!
55//!     Ok(())
56//! }
57//! ```
58//!
59//! [new-type]: https://doc.rust-lang.org/book/ch19-04-advanced-types.html#using-the-newtype-pattern-for-type-safety-and-abstraction
60
61pub use serde_json::{value::RawValue, Error, Result};
62pub use serialize_to_javascript_impl::{default_template, Template};
63
64use std::fmt;
65
66#[doc(hidden)]
67pub mod private;
68
69/// JavaScript code (in the form of a function parameter) for the JSON.parse() reviver.
70const FREEZE_REVIVER: &str = ",(_,v)=>Object.freeze(v)";
71
72/// Serialized JavaScript output.
73#[derive(Debug, Clone)]
74pub struct Serialized(String);
75
76impl fmt::Display for Serialized {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        self.0.fmt(f)
79    }
80}
81
82impl Serialized {
83    /// Create a new [`Serialized`] from the inputs.
84    #[inline(always)]
85    pub fn new(json: &RawValue, options: &Options) -> Self {
86        escape_json_parse(json, options)
87    }
88
89    /// Get the inner [`String`] out.
90    #[inline(always)]
91    pub fn into_string(self) -> String {
92        self.0
93    }
94}
95
96/// Optional settings to pass to the templating system.
97#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
98pub struct Options {
99    /// If the parsed JSON will be frozen with [`Object.freeze()`].
100    ///
101    /// [`Object.freeze()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze
102    #[allow(dead_code)]
103    pub freeze: bool,
104
105    /// _Extra_ amount of bytes to allocate to the String buffer during serialization.
106    ///
107    /// Note: This is not the total buffer size, but the extra buffer size created. By default the
108    /// buffer size will already be enough to not need to allocate more than once for input that
109    /// does not need escaping. Therefore, this extra buffer is more of "how many bytes of escaped
110    /// characters do I want to prepare for?"
111    pub buf: usize,
112}
113
114/// A struct that contains [`serde::Serialize`] data to insert into a template.
115///
116/// Create this automatically with a `#[derive(Template)]` attribute. All fields not marked `#[raw]`
117/// will be compile-time checked that they implement [`serde::Serialize`].
118///
119/// Due to the nature of templating variables, [tuple structs] are not allowed as their fields
120/// have no names. [Unit structs] have no fields and are a valid target of this trait.
121///
122/// Template variables are generated as `__TEMPLATE_my_field__` where the serialized value of the
123/// `my_field` field replaces all instances of the template variable.
124///
125/// # Raw Values
126///
127/// If you have raw values you would like to inject into the template that is not serializable
128/// through JSON, such as a string of JavaScript code, then you can mark a field with `#[raw]` to
129/// make it embedded directly. **Absolutely NO serialization occurs**, the field is just turned into
130/// a string using [`Display`]. As such, fields that are marked `#[raw]` _only_ require [`Display`].
131///
132/// Raw values use `__RAW_my_field__` as the template variable.
133///
134/// ---
135///
136/// This trait is sealed.
137///
138/// [tuple structs]: https://doc.rust-lang.org/book/ch05-01-defining-structs.html#using-tuple-structs-without-named-fields-to-create-different-types
139/// [`Display`]: std::fmt::Display
140pub trait Template: self::private::Sealed {
141    /// Render the serialized template data into the passed template.
142    fn render(&self, template: &str, options: &Options) -> Result<Serialized>;
143}
144
145/// A [`Template`] with an attached default template.
146///
147/// Create this automatically with `#[default_template("myfile.js")` on your [`Template`] struct.
148pub trait DefaultTemplate: Template {
149    /// The raw static string with the templates contents.
150    ///
151    /// When using `#[default_template("myfile.js")]` it will be generated as
152    /// `include_str!("myfile.js")`.
153    const RAW_TEMPLATE: &'static str;
154
155    /// Render the serialized template data into the default template.
156    ///
157    /// If this method is implemented manually, it still needs to use [`Template::render`] to be
158    /// serialized correctly.
159    fn render_default(&self, options: &Options) -> Result<Serialized> {
160        self.render(Self::RAW_TEMPLATE, options)
161    }
162}
163
164/// Estimated the minimum capacity needed for the serialized string based on inputs.
165///
166/// This size will include the size of the wrapping JavaScript (`JSON.parse()` and a potential
167/// reviver function based on options) and the user supplied `buf_size` from the passed [`Options`].
168/// It currently estimates the minimum size of the passed JSON by assuming it does not need escaping
169/// and taking the length of the `&str`.
170fn estimated_capacity(json: &RawValue, options: &Options) -> usize {
171    // 14 chars in JSON.parse('')
172    let mut buf = 14;
173
174    // we know it's at least going to contain the length of the json
175    buf += json.get().len();
176
177    // add in user defined extra buffer size
178    buf += options.buf;
179
180    // freezing code expands the output size due to the embedded reviver code
181    if options.freeze {
182        buf += FREEZE_REVIVER.len();
183    }
184
185    buf
186}
187
188/// Transforms & escapes a JSON String to `JSON.parse('{json}')`
189///
190/// Single quotes chosen because double quotes are already used in JSON. With single quotes, we only
191/// need to escape strings that include backslashes or single quotes. If we used double quotes, then
192/// there would be no cases that a string doesn't need escaping.
193///
194/// # Safety
195///
196/// The ability to safely escape JSON into a JSON.parse('{json}') relies entirely on 2 things.
197///
198/// 1. `serde_json`'s ability to correctly escape and format JSON into a [`String`].
199/// 2. JavaScript engines not accepting anything except another unescaped, literal single quote
200///     character to end a string that was opened with it.
201///
202/// # Allocations
203///
204/// A new [`String`] will always be allocated. If `buf_size` is set to `0`, then it will by default
205/// allocate to the return value of [`estimated_capacity()`].
206fn escape_json_parse(json: &RawValue, options: &Options) -> Serialized {
207    let capacity = estimated_capacity(json, options);
208    let json = json.get();
209
210    let mut buf = String::with_capacity(capacity);
211    buf.push_str("JSON.parse('");
212
213    // insert a backslash before any backslash or single quote characters to escape them
214    let mut last = 0;
215    for (idx, _) in json.match_indices(|c| c == '\\' || c == '\'') {
216        buf.push_str(&json[last..idx]);
217        buf.push('\\');
218        last = idx;
219    }
220
221    // finish appending the trailing json characters that don't need escaping
222    buf.push_str(&json[last..]);
223
224    // close out the escaped JavaScript string
225    buf.push('\'');
226
227    // custom reviver to freeze all parsed items
228    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#using_the_reviver_parameter
229    if options.freeze {
230        buf.push_str(FREEZE_REVIVER);
231    }
232
233    // finish the JSON.parse() call
234    buf.push(')');
235
236    Serialized(buf)
237}