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}