Skip to main content

fiftyone_javascript_builder/
template_data.rs

1/* *********************************************************************
2 * This Original Work is copyright of 51 Degrees Mobile Experts Limited.
3 * Copyright 2026 51 Degrees Mobile Experts Limited, Davidson House,
4 * Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU.
5 *
6 * This Original Work is licensed under the European Union Public Licence
7 * (EUPL) v.1.2 and is subject to its terms as set out below.
8 *
9 * If a copy of the EUPL was not distributed with this file, You can obtain
10 * one at https://opensource.org/licenses/EUPL-1.2.
11 *
12 * The 'Compatible Licences' set out in the Appendix to the EUPL (as may be
13 * amended by the European Commission) shall be deemed incompatible for
14 * the purposes of the Work and the provisions of the compatibility
15 * clause in Article 5 of the EUPL shall not apply.
16 *
17 * If using the Work as, or as part of, a network application, by
18 * including the attribution notice(s) required under Article 5 of the EUPL
19 * in the end user terms of the application under an appropriate heading,
20 * such notice(s) shall fulfill the requirements of that article.
21 * ********************************************************************* */
22
23//! The render context for the bundled Mustache template.
24//!
25//! This packages the eleven parameters the template expects and renders them
26//! with the
27//! crate's small [`crate::mustache`] renderer, implementing its [`Context`]
28//! trait so that HTML escaping is disabled for every field.
29
30use crate::constants::MISSING_JSON_OBJECT;
31use crate::mustache::{Context, Template, Value};
32
33/// The parameters required by the `JavaScriptResource.mustache` template.
34///
35/// The field names match the Mustache variable names exactly (`_objName`,
36/// `_jsonObject`, ...). Every string field is emitted without HTML escaping so
37/// the JSON payload, the callback URL and the object name reach the client
38/// verbatim.
39///
40/// Construct it with [`JavaScriptResource::new`], which applies the
41/// missing-JSON fallback, then render it through
42/// [`JavaScriptResource::render`].
43#[derive(Debug, Clone)]
44pub struct JavaScriptResource {
45    /// The name of the global-scope object the client JavaScript creates.
46    obj_name: String,
47    /// The JSON data payload inserted into the template.
48    json_object: String,
49    /// The session id used in the JavaScript response.
50    session_id: String,
51    /// The sequence value used in the JavaScript response.
52    sequence: i32,
53    /// Whether to produce JavaScript that uses promises.
54    supports_promises: bool,
55    /// Whether to produce JavaScript that uses the fetch API.
56    supports_fetch: bool,
57    /// The callback URL, empty when no valid URL could be built.
58    url: String,
59    /// The request parameters appended to the callback URL, as a JSON object.
60    parameters: String,
61    /// Whether client-side processing stores results in cookies.
62    enable_cookies: bool,
63    /// Whether the background callback mechanism is enabled.
64    update_enabled: bool,
65    /// Whether the payload contains delayed-execution JavaScript properties.
66    has_delayed_properties: bool,
67}
68
69impl JavaScriptResource {
70    /// Build a render context.
71    ///
72    /// `json_object` is replaced with the missing-JSON placeholder when it is
73    /// empty or whitespace.
74    /// `url` is the already-built callback URL (empty string when none could be
75    /// formed). `update_enabled` should be `true` only when that URL is present.
76    #[allow(clippy::too_many_arguments)]
77    pub fn new(
78        obj_name: impl Into<String>,
79        json_object: impl Into<String>,
80        session_id: impl Into<String>,
81        sequence: i32,
82        supports_promises: bool,
83        supports_fetch: bool,
84        url: impl Into<String>,
85        parameters: impl Into<String>,
86        enable_cookies: bool,
87        update_enabled: bool,
88        has_delayed_properties: bool,
89    ) -> Self {
90        let json_object = json_object.into();
91        let json_object = if json_object.trim().is_empty() {
92            MISSING_JSON_OBJECT.to_owned()
93        } else {
94            json_object
95        };
96        JavaScriptResource {
97            obj_name: obj_name.into(),
98            json_object,
99            session_id: session_id.into(),
100            sequence,
101            supports_promises,
102            supports_fetch,
103            url: url.into(),
104            parameters: parameters.into(),
105            enable_cookies,
106            update_enabled,
107            has_delayed_properties,
108        }
109    }
110
111    /// Render the supplied template with this context, with HTML escaping
112    /// disabled for every field.
113    ///
114    /// Rendering into a `String` is infallible, so this returns the rendered
115    /// content directly.
116    pub fn render(&self, template: &Template) -> String {
117        template.render(self)
118    }
119}
120
121impl Context for JavaScriptResource {
122    fn value(&self, name: &str) -> Option<Value<'_>> {
123        // Every field is emitted verbatim (no HTML escaping). The boolean fields
124        // are only used
125        // as sections, but answering them here as their lowercase JavaScript
126        // spelling keeps the context complete.
127        match name {
128            "_objName" => Some(Value::Str(&self.obj_name)),
129            "_jsonObject" => Some(Value::Str(&self.json_object)),
130            "_sessionId" => Some(Value::Str(&self.session_id)),
131            // The sequence is a bare integer in the template
132            // (`var sequence = {{&_sequence}};`).
133            "_sequence" => Some(Value::Int(self.sequence as i64)),
134            "_url" => Some(Value::Str(&self.url)),
135            "_parameters" => Some(Value::Str(&self.parameters)),
136            "_supportsPromises" => Some(Value::Str(bool_str(self.supports_promises))),
137            "_supportsFetch" => Some(Value::Str(bool_str(self.supports_fetch))),
138            "_enableCookies" => Some(Value::Str(bool_str(self.enable_cookies))),
139            "_updateEnabled" => Some(Value::Str(bool_str(self.update_enabled))),
140            "_hasDelayedProperties" => Some(Value::Str(bool_str(self.has_delayed_properties))),
141            _ => None,
142        }
143    }
144
145    fn flag(&self, name: &str) -> Option<bool> {
146        match name {
147            "_supportsPromises" => Some(self.supports_promises),
148            "_supportsFetch" => Some(self.supports_fetch),
149            "_enableCookies" => Some(self.enable_cookies),
150            "_updateEnabled" => Some(self.update_enabled),
151            "_hasDelayedProperties" => Some(self.has_delayed_properties),
152            _ => None,
153        }
154    }
155}
156
157/// The lowercase JavaScript spelling of a boolean.
158fn bool_str(value: bool) -> &'static str {
159    if value {
160        "true"
161    } else {
162        "false"
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    fn resource(json: &str) -> JavaScriptResource {
171        JavaScriptResource::new(
172            "fod",
173            json,
174            "sid",
175            1,
176            false,
177            false,
178            "https://h/json",
179            "{}",
180            true,
181            true,
182            false,
183        )
184    }
185
186    #[test]
187    fn missing_json_falls_back_to_placeholder() {
188        let resource = resource("   ");
189        assert_eq!(resource.json_object, MISSING_JSON_OBJECT);
190    }
191
192    #[test]
193    fn fields_are_not_html_escaped() {
194        // A JSON payload with characters that would be HTML-escaped by default.
195        let source = "var json = {{&_jsonObject}}; var name = \"{{_objName}}\";";
196        let template = Template::parse(source).unwrap();
197        let resource = JavaScriptResource::new(
198            "ob<j>",
199            "{\"a\":\"<b>&\\\"\"}",
200            "sid",
201            1,
202            false,
203            false,
204            "",
205            "{}",
206            true,
207            false,
208            false,
209        );
210        let rendered = resource.render(&template);
211        // No HTML entities anywhere: the '<', '>', '&' and '"' survive verbatim.
212        assert!(!rendered.contains("&lt;"));
213        assert!(!rendered.contains("&gt;"));
214        assert!(!rendered.contains("&amp;"));
215        assert!(!rendered.contains("&quot;"));
216        assert!(rendered.contains("ob<j>"));
217        assert!(rendered.contains("{\"a\":\"<b>&\\\"\"}"));
218    }
219
220    #[test]
221    fn boolean_sections_render_their_body() {
222        let source =
223            "{{#_enableCookies}}YES{{/_enableCookies}}{{^_enableCookies}}NO{{/_enableCookies}}";
224        let template = Template::parse(source).unwrap();
225
226        let cookies_on = JavaScriptResource::new(
227            "fod", "{}", "", 1, false, false, "", "{}", true, false, false,
228        );
229        assert_eq!(cookies_on.render(&template), "YES");
230
231        let cookies_off = JavaScriptResource::new(
232            "fod", "{}", "", 1, false, false, "", "{}", false, false, false,
233        );
234        assert_eq!(cookies_off.render(&template), "NO");
235    }
236
237    #[test]
238    fn sequence_renders_as_bare_integer() {
239        let source = "var sequence = {{&_sequence}};";
240        let template = Template::parse(source).unwrap();
241        let resource = JavaScriptResource::new(
242            "fod", "{}", "", 7, false, false, "", "{}", true, false, false,
243        );
244        assert_eq!(resource.render(&template), "var sequence = 7;");
245    }
246}