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