neutralts/
template.rs

1use crate::{
2    block_parser::BlockInherit, block_parser::BlockParser, constants::*, default_json::*,
3    shared::Shared, utils::*,
4};
5use regex::Regex;
6use serde_json::{json, Value};
7use std::fs;
8use std::path::Path;
9use std::sync::OnceLock;
10use std::time::{Duration, Instant};
11use std::rc::Rc;
12
13pub struct Template<'a> {
14    raw: String,
15    file_path: &'a str,
16    schema: Value,
17    shared: Shared,
18    time_start: Instant,
19    time_elapsed: Duration,
20    out: String,
21}
22
23fn default_schema_template() -> Result<Value, String> {
24    static DEFAULT_SCHEMA: OnceLock<Result<Value, String>> = OnceLock::new();
25    DEFAULT_SCHEMA
26        .get_or_init(|| {
27            serde_json::from_str(DEFAULT)
28                .map_err(|_| "const DEFAULT is not a valid JSON string".to_string())
29        })
30        .clone()
31}
32
33/// A struct representing a template that can be rendered.
34///
35/// This struct is used to handle the rendering of templates.
36impl<'a> Template<'a> {
37    /// Constructs a new `Template` instance with default settings.
38    ///
39    /// It allows you to set up a template and schema with different types.
40    pub fn new() -> Result<Self, String> {
41        let default_schema = default_schema_template()?;
42        let shared = Shared::new(default_schema.clone());
43
44        Ok(Template {
45            raw: String::new(),
46            file_path: "",
47            schema: default_schema,
48            shared,
49            time_start: Instant::now(),
50            time_elapsed: Instant::now().elapsed(),
51            out: String::new(),
52        })
53    }
54
55    /// Constructs a new `Template` instance from a file path and a JSON schema.
56    ///
57    /// # Arguments
58    ///
59    /// * `file_path` - A reference to the path of the file containing the template content.
60    /// * `schema` - A JSON value representing the custom schema to be used with the template.
61    ///
62    /// # Returns
63    ///
64    /// A `Result` containing the new `Template` instance or an error message if:
65    /// - The file cannot be read.
66    pub fn from_file_value(file_path: &'a str, schema: Value) -> Result<Self, String> {
67        let raw: String = match fs::read_to_string(file_path) {
68            Ok(s) => s,
69            Err(e) => {
70                eprintln!("Cannot be read: {}", file_path);
71                return Err(e.to_string());
72            }
73        };
74        let mut default_schema = default_schema_template()?;
75
76        update_schema_owned(&mut default_schema, schema);
77        // Avoid cloning a potentially huge merged schema during construction.
78        // `shared` will be fully initialized in `init_render` when needed.
79        let shared = Shared::new(default_schema_template()?);
80
81        Ok(Template {
82            raw,
83            file_path,
84            schema: default_schema,
85            shared,
86            time_start: Instant::now(),
87            time_elapsed: Instant::now().elapsed(),
88            out: String::new(),
89        })
90    }
91
92    /// Sets the source path of the template.
93    ///
94    /// # Arguments
95    ///
96    /// * `file_path` - A reference to the path of the file containing the template content.
97    ///
98    /// # Returns
99    ///
100    /// A `Result` indicating success or an error message if the file cannot be read
101    pub fn set_src_path(&mut self, file_path: &'a str) -> Result<(), String> {
102        self.file_path = file_path;
103        self.raw = match fs::read_to_string(file_path) {
104            Ok(s) => s,
105            Err(e) => {
106                eprintln!("Cannot be read: {}", file_path);
107                return Err(e.to_string());
108            }
109        };
110
111        Ok(())
112    }
113
114    /// Sets the content of the template from a string.
115    ///
116    /// # Arguments
117    ///
118    /// * `source` - A reference to the new string content to be set as the raw content.
119    pub fn set_src_str(&mut self, source: &str) {
120        self.raw = source.to_string();
121    }
122
123    /// Merges the schema from a file with the current template schema.
124    ///
125    /// # Arguments
126    ///
127    /// * `schema_path` - A reference to the path of the file containing the schema content.
128    ///
129    /// # Returns
130    ///
131    /// A `Result` indicating success or an error message if:
132    /// - The file cannot be read.
133    /// - The file's content is not a valid JSON string.
134    pub fn merge_schema_path(&mut self, schema_path: &str) -> Result<(), String> {
135        let schema_bytes = match fs::read(schema_path) {
136            Ok(bytes) => bytes,
137            Err(e) => {
138                eprintln!("Cannot be read: {}", schema_path);
139                return Err(e.to_string());
140            }
141        };
142        let schema_value: Value = match serde_json::from_slice(&schema_bytes) {
143            Ok(value) => value,
144            Err(_) => {
145                return Err("Is not a valid JSON file".to_string());
146            }
147        };
148        update_schema_owned(&mut self.schema, schema_value);
149
150        Ok(())
151    }
152
153    /// Merges the schema from a JSON string with the current template schema.
154    ///
155    /// # Arguments
156    ///
157    /// * `schema` - A reference to the JSON string of the schema content.
158    ///
159    /// # Returns
160    ///
161    /// A `Result` indicating success or an error message if:
162    /// - The file's content is not a valid JSON string.
163    pub fn merge_schema_str(&mut self, schema: &str) -> Result<(), String> {
164        let schema_value: Value = match serde_json::from_str(schema) {
165            Ok(value) => value,
166            Err(_) => {
167                return Err("Is not a valid JSON string".to_string());
168            }
169        };
170        update_schema_owned(&mut self.schema, schema_value);
171
172        Ok(())
173    }
174
175    /// Merges the provided JSON value with the current schema.
176    ///
177    /// # Arguments
178    ///
179    /// * `schema` - The JSON Value to be merged with the current schema.
180    pub fn merge_schema_value(&mut self, schema: Value) {
181        update_schema_owned(&mut self.schema, schema);
182    }
183
184    /// Constructs a new `Template` instance from a file path and MessagePack schema bytes.
185    ///
186    /// # Arguments
187    ///
188    /// * `file_path` - A reference to the path of the file containing the template content.
189    /// * `bytes` - A byte slice containing the MessagePack schema.
190    ///
191    /// # Returns
192    ///
193    /// A `Result` containing the new `Template` instance or an error message if:
194    /// - The template file cannot be read.
195    /// - The MessagePack data is invalid.
196    ///
197    /// # Example
198    ///
199    /// ```no_run
200    /// use neutralts::Template;
201    /// let bytes = vec![129, 164, 100, 97, 116, 97, 129, 163, 107, 101, 121, 165, 118, 97, 108, 117, 101];
202    /// let template = Template::from_file_msgpack("template.ntpl", &bytes).unwrap();
203    /// ```
204    pub fn from_file_msgpack(file_path: &'a str, bytes: &[u8]) -> Result<Self, String> {
205        let schema: Value = if bytes.is_empty() {
206            json!({})
207        } else {
208            match rmp_serde::from_slice(bytes) {
209                Ok(v) => v,
210                Err(e) => return Err(format!("Invalid MessagePack data: {}", e)),
211            }
212        };
213
214        Self::from_file_value(file_path, schema)
215    }
216
217    /// Merges the schema from a MessagePack file with the current template schema.
218    ///
219    /// # Arguments
220    ///
221    /// * `msgpack_path` - A reference to the path of the file containing the MessagePack schema.
222    ///
223    /// # Returns
224    ///
225    /// A `Result` indicating success or an error message if:
226    /// - The file cannot be read.
227    /// - The file's content is not a valid MessagePack.
228    ///
229    /// # Example
230    ///
231    /// ```no_run
232    /// use neutralts::Template;
233    /// let mut template = Template::new().unwrap();
234    /// template.merge_schema_msgpack_path("extra_data.msgpack").unwrap();
235    /// ```
236    pub fn merge_schema_msgpack_path(&mut self, msgpack_path: &str) -> Result<(), String> {
237        let msgpack_data = match fs::read(msgpack_path) {
238            Ok(data) => data,
239            Err(e) => {
240                eprintln!("Cannot be read: {}", msgpack_path);
241                return Err(e.to_string());
242            }
243        };
244
245        self.merge_schema_msgpack(&msgpack_data)
246    }
247
248    /// Merges the schema from MessagePack bytes with the current template schema.
249    ///
250    /// # Arguments
251    ///
252    /// * `bytes` - A byte slice containing the MessagePack schema.
253    ///
254    /// # Returns
255    ///
256    /// A `Result` indicating success or an error message if:
257    /// - The bytes are not a valid MessagePack.
258    ///
259    /// # Example
260    ///
261    /// ```
262    /// use neutralts::Template;
263    /// let mut template = Template::new().unwrap();
264    /// let bytes = vec![129, 164, 100, 97, 116, 97, 129, 163, 107, 101, 121, 165, 118, 97, 108, 117, 101];
265    /// template.merge_schema_msgpack(&bytes).unwrap();
266    /// ```
267    pub fn merge_schema_msgpack(&mut self, bytes: &[u8]) -> Result<(), String> {
268        let schema_value: Value = match rmp_serde::from_slice(bytes) {
269            Ok(value) => value,
270            Err(e) => {
271                return Err(format!("Is not a valid MessagePack data: {}", e));
272            }
273        };
274        update_schema_owned(&mut self.schema, schema_value);
275
276        Ok(())
277    }
278
279    /// Renders the template content.
280    ///
281    /// This function initializes the rendering process.
282    /// The resulting output is returned as a string.
283    ///
284    /// # Returns
285    ///
286    /// The rendered template content as a string.
287    pub fn render(&mut self) -> String {
288        // Fast path: when there are no blocks, skip full render initialization.
289        // This avoids cloning large schemas for templates with plain text/empty source.
290        self.time_start = Instant::now();
291        if !self.raw.contains(BIF_OPEN) {
292            self.out = self.raw.trim().to_string();
293            self.time_elapsed = self.time_start.elapsed();
294            return self.out.clone();
295        }
296
297        let inherit = self.init_render();
298        self.out = BlockParser::new(&mut self.shared, inherit.clone()).parse(&self.raw, "");
299
300        while self.out.contains("{:!cache;") {
301            let out;
302            out = BlockParser::new(&mut self.shared, inherit.clone()).parse(&self.out, "!cache");
303            self.out = out;
304        }
305
306        self.ends_render();
307
308        self.out.clone()
309    }
310
311    /// Renders the template content without cloning the schema.
312    ///
313    /// This is an optimized version of `render()` that takes ownership of the schema
314    /// instead of cloning it. Use this when you only need to render once per template
315    /// instance, which is the most common use case in web applications.
316    ///
317    /// # When to Use
318    ///
319    /// - **Single render per request**: Most web applications create a template, render it once,
320    ///   and discard it. This is the ideal use case for `render_once()`.
321    /// - **Large schemas**: When your schema contains thousands of keys, the performance
322    ///   improvement can be 5-10x faster than `render()`.
323    /// - **Memory-constrained environments**: Avoids the memory spike of cloning large schemas.
324    ///
325    /// # When NOT to Use
326    ///
327    /// - **Multiple renders**: If you need to render the same template multiple times with
328    ///   the same schema, use `render()` instead.
329    /// - **Template reuse**: After `render_once()`, the template cannot be reused because
330    ///   the schema is consumed.
331    ///
332    /// # Performance
333    ///
334    /// Benchmarks show significant improvements for large schemas:
335    /// - 100 keys: ~3.7x faster
336    /// - 500 keys: ~7x faster
337    /// - 1000+ keys: ~10x faster
338    ///
339    /// # Post-Call Behavior
340    ///
341    /// After calling this method, the template's schema will be empty (`{}`) and subsequent
342    /// calls to `render()` or `render_once()` will produce empty output for schema variables.
343    /// The template struct itself remains valid but should be discarded after use.
344    ///
345    /// # Example
346    ///
347    /// ```rust
348    /// use neutralts::Template;
349    ///
350    /// let schema = serde_json::json!({
351    ///     "data": {
352    ///         "title": "Hello World"
353    ///     }
354    /// });
355    ///
356    /// let mut template = Template::new().unwrap();
357    /// template.merge_schema_value(schema);
358    /// template.set_src_str("{:;title:}");
359    ///
360    /// // Single render - use render_once() for best performance
361    /// let output = template.render_once();
362    /// assert!(output.contains("Hello World"));
363    ///
364    /// // Template should NOT be reused after render_once()
365    /// // Create a new Template instance for the next render
366    /// ```
367    ///
368    /// # Returns
369    ///
370    /// The rendered template content as a string.
371    pub fn render_once(&mut self) -> String {
372        // Fast path: when there are no blocks, skip full render initialization.
373        self.time_start = Instant::now();
374        if !self.raw.contains(BIF_OPEN) {
375            self.out = self.raw.trim().to_string();
376            self.time_elapsed = self.time_start.elapsed();
377            return self.out.clone();
378        }
379
380        let inherit = self.init_render_once();
381        self.out = BlockParser::new(&mut self.shared, inherit.clone()).parse(&self.raw, "");
382
383        while self.out.contains("{:!cache;") {
384            let out;
385            out = BlockParser::new(&mut self.shared, inherit.clone()).parse(&self.out, "!cache");
386            self.out = out;
387        }
388
389        self.ends_render();
390
391        self.out.clone()
392    }
393
394    // Restore vars for render (clones schema for reusability)
395    fn init_render(&mut self) -> BlockInherit {
396        self.time_start = Instant::now();
397        self.shared = Shared::new(self.schema.clone());
398
399        if self.shared.comments.contains("remove") {
400            self.raw = remove_comments(&self.raw);
401        }
402
403        // init inherit
404        let mut inherit = BlockInherit::new();
405        let indir = inherit.create_block_schema(&mut self.shared);
406        self.shared.schema["__moveto"] = json!({});
407        self.shared.schema["__error"] = json!([]);
408        self.shared.indir_store.clear();
409        self.shared.indir_store.insert(indir, Rc::new(self.shared.schema["inherit"].clone()));
410        inherit.current_file = self.file_path.to_string();
411
412        // Escape CONTEXT values
413        filter_value(&mut self.shared.schema["data"]["CONTEXT"]);
414
415        // Escape CONTEXT keys names
416        filter_value_keys(&mut self.shared.schema["data"]["CONTEXT"]);
417
418        if !self.file_path.is_empty() {
419            let path = Path::new(&self.file_path);
420
421            if let Some(parent) = path.parent() {
422                inherit.current_dir = parent.display().to_string();
423            }
424        } else {
425            inherit.current_dir = self.shared.working_dir.clone();
426        }
427
428        if !self.shared.debug_file.is_empty() {
429            eprintln!("WARNING: config->debug_file is not empty: {} (Remember to remove this in production)", self.shared.debug_file);
430        }
431
432        inherit
433    }
434
435    // Restore vars for render_once (takes ownership of schema, no clone)
436    fn init_render_once(&mut self) -> BlockInherit {
437        self.time_start = Instant::now();
438        // Take ownership of schema instead of cloning - leaves empty object in place
439        let schema = std::mem::take(&mut self.schema);
440        self.shared = Shared::new(schema);
441
442        if self.shared.comments.contains("remove") {
443            self.raw = remove_comments(&self.raw);
444        }
445
446        // init inherit
447        let mut inherit = BlockInherit::new();
448        let indir = inherit.create_block_schema(&mut self.shared);
449        self.shared.schema["__moveto"] = json!({});
450        self.shared.schema["__error"] = json!([]);
451        self.shared.indir_store.clear();
452        self.shared.indir_store.insert(indir, Rc::new(self.shared.schema["inherit"].clone()));
453        inherit.current_file = self.file_path.to_string();
454
455        // Escape CONTEXT values
456        filter_value(&mut self.shared.schema["data"]["CONTEXT"]);
457
458        // Escape CONTEXT keys names
459        filter_value_keys(&mut self.shared.schema["data"]["CONTEXT"]);
460
461        if !self.file_path.is_empty() {
462            let path = Path::new(&self.file_path);
463
464            if let Some(parent) = path.parent() {
465                inherit.current_dir = parent.display().to_string();
466            }
467        } else {
468            inherit.current_dir = self.shared.working_dir.clone();
469        }
470
471        if !self.shared.debug_file.is_empty() {
472            eprintln!("WARNING: config->debug_file is not empty: {} (Remember to remove this in production)", self.shared.debug_file);
473        }
474
475        inherit
476    }
477
478    // Rendering ends
479    fn ends_render(&mut self) {
480        self.set_moveto();
481        self.replacements();
482        self.set_status_code();
483        self.time_elapsed = self.time_start.elapsed();
484    }
485
486    fn set_status_code(&mut self) {
487        let status_code = self.shared.status_code.as_str();
488
489        if ("400"..="599").contains(&status_code) {
490            self.out = format!("{} {}", self.shared.status_code, self.shared.status_text);
491
492            return;
493        }
494
495        if status_code == "301"
496            || status_code == "302"
497            || status_code == "303"
498            || status_code == "307"
499            || status_code == "308"
500        {
501            self.out = format!(
502                "{} {}\n{}",
503                self.shared.status_code, self.shared.status_text, self.shared.status_param
504            );
505
506            return;
507        }
508
509        if !self.shared.redirect_js.is_empty() {
510            self.out = self.shared.redirect_js.clone();
511        }
512    }
513
514    fn set_moveto(&mut self) {
515        if let Value::Object(data_map) = &self.shared.schema["__moveto"] {
516            for (_key, value) in data_map {
517                if let Value::Object(inner_map) = value {
518                    for (inner_key, inner_value) in inner_map {
519                        let mut tag;
520
521                        // although it should be "<tag" or "</tag" it also supports
522                        // "tag", "/tag", "<tag>" and "</tag>
523                        if !inner_key.starts_with("<") {
524                            tag = format!("<{}", inner_key);
525                        } else {
526                            tag = inner_key.to_string();
527                        }
528                        if tag.ends_with(">") {
529                            tag = tag[..tag.len() - 1].to_string();
530                        }
531
532                        // if it does not find it, it does nothing
533                        let position = find_tag_position(&self.out, &tag);
534                        if let Some(pos) = position {
535                            let mut insert = inner_value.as_str().unwrap().to_string();
536                            insert = insert.to_string();
537                            self.out.insert_str(pos, &insert);
538                        }
539                    }
540                }
541            }
542        }
543    }
544
545    fn replacements(&mut self) {
546        if self.out.contains(BACKSPACE) {
547            lazy_static::lazy_static! {
548                static ref RE: Regex = Regex::new(&format!(r"\s*{}", BACKSPACE)).expect("Failed to create regex with constant pattern");
549            }
550            if let std::borrow::Cow::Owned(s) = RE.replace_all(&self.out, "") {
551                self.out = s;
552            }
553        }
554
555        // UNPRINTABLE should be substituted after BACKSPACE
556        if self.out.contains(UNPRINTABLE) {
557            self.out = self.out.replace(UNPRINTABLE, "");
558        }
559    }
560
561    /// Retrieves the status code.
562    ///
563    /// The status code is "200" unless "exit", "redirect" is used or the
564    /// template contains a syntax error, which will return a status code
565    /// of "500". Although the codes are numeric, a string is returned.
566    ///
567    /// # Returns
568    ///
569    /// A reference to the status code as a string.
570    pub fn get_status_code(&self) -> &String {
571        &self.shared.status_code
572    }
573
574    /// Retrieves the status text.
575    ///
576    /// It will correspond to the one set by the HTTP protocol.
577    ///
578    /// # Returns
579    ///
580    /// A reference to the status text as a string.
581    pub fn get_status_text(&self) -> &String {
582        &self.shared.status_text
583    }
584
585    /// Retrieves the status parameter.
586    ///
587    /// Some statuses such as 301 (redirect) may contain additional data, such
588    /// as the destination URL, and in similar cases “param” will contain
589    /// that value.
590    ///
591    /// # Returns
592    ///
593    /// A reference to the status parameter as a string.
594    pub fn get_status_param(&self) -> &String {
595        &self.shared.status_param
596    }
597
598    /// Checks if there is an error.
599    ///
600    /// If any error has occurred, in the parse or otherwise, it will return true.
601    ///
602    /// # Returns
603    ///
604    /// A boolean indicating whether there is an error.
605    pub fn has_error(&self) -> bool {
606        self.shared.has_error
607    }
608
609    /// Get bifs errors list
610    ///
611    /// # Returns
612    ///
613    /// * `Value`: A clone of the value with the list of errors in the bifs during rendering.
614    pub fn get_error(&self) -> Value {
615        self.shared.schema["__error"].clone()
616    }
617
618    /// Retrieves the time duration for template rendering.
619    ///
620    /// # Returns
621    ///
622    /// The time duration elapsed .
623    pub fn get_time_duration(&self) -> Duration {
624        let duration: std::time::Duration = self.time_elapsed;
625
626        duration
627    }
628}