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("<"));
213 assert!(!rendered.contains(">"));
214 assert!(!rendered.contains("&"));
215 assert!(!rendered.contains("""));
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}