tera_rand/
file.rs

1use crate::common::parse_arg;
2use crate::error::{empty_file, internal_error, missing_arg, read_file_error};
3use dashmap::mapref::one::Ref;
4use dashmap::DashMap;
5use lazy_static::lazy_static;
6use rand::{rng, Rng};
7use std::collections::HashMap;
8use std::fs::File;
9use std::io::{BufRead, BufReader};
10use tera::{to_value, Result, Value};
11
12lazy_static! {
13    static ref FILE_CACHE: DashMap<String, Vec<String>> = DashMap::new();
14}
15
16/// A Tera function to sample a random value from a line-delimited file of strings. The filepath
17/// should be passed in as an argument to the `path` parameter.
18///
19/// Note that the contents of the filepath is read only once and cached.
20///
21/// # Example usage
22///
23/// ```edition2021
24/// use tera::{Context, Tera};
25/// use tera_rand::random_from_file;
26///
27/// let mut tera: Tera = Tera::default();
28/// tera.register_function("random_from_file", random_from_file);
29/// let context: Context = Context::new();
30///
31/// let rendered: String = tera
32///     .render_str(r#"{{ random_from_file(path="resources/test/addresses.txt") }}"#, &context)
33///     .unwrap();
34/// ```
35pub fn random_from_file(args: &HashMap<String, Value>) -> Result<Value> {
36    let filepath: Option<String> = parse_arg(args, "path")?;
37    let filepath: String = filepath.ok_or_else(|| missing_arg("path"))?;
38
39    let possible_values_ref: Ref<String, Vec<String>> = read_all_file_lines(filepath)?;
40    let possible_values: &Vec<String> = possible_values_ref.value();
41
42    let index_to_sample: usize = rng().random_range(0usize..possible_values.len());
43    convert_line_to_json_value(possible_values_ref.key(), possible_values, index_to_sample)
44}
45
46/// A Tera function to sample a specific value from a line-delimited file of strings. The filepath
47/// should be passed in as an argument to the `path` parameter. The 0-indexed line number should
48/// be passed in as an argument to the `line_num` parameter.
49///
50/// Note that the contents of the filepath is read only once and cached.
51///
52/// # Example usage
53///
54/// ```edition2021
55/// use tera::{Context, Tera};
56/// use tera_rand::line_from_file;
57/// let mut tera: Tera = Tera::default();
58/// tera.register_function("line_from_file", line_from_file);
59/// let context: Context = Context::new();
60/// let rendered: String = tera
61///     .render_str(
62///         r#"{{ line_from_file(path="resources/test/addresses.txt", line_num=2) }}"#,
63///         &context
64///     )
65///     .unwrap();
66/// ```
67pub fn line_from_file(args: &HashMap<String, Value>) -> Result<Value> {
68    let filepath_opt: Option<String> = parse_arg(args, "path")?;
69    let filepath: String = filepath_opt.ok_or_else(|| missing_arg("path"))?;
70
71    let line_num: Option<usize> = parse_arg(args, "line_num")?;
72    let line_num: usize = line_num.ok_or_else(|| missing_arg("line_num"))?;
73
74    let possible_values_ref = read_all_file_lines(filepath)?;
75    let possible_values: &Vec<String> = possible_values_ref.value();
76
77    convert_line_to_json_value(possible_values_ref.key(), possible_values, line_num)
78}
79
80fn convert_line_to_json_value(
81    filename: &String,
82    possible_values: &Vec<String>,
83    line_num: usize
84) -> Result<Value> {
85    match possible_values.get(line_num) {
86        Some(sampled_value) => {
87            let json_value = to_value(sampled_value)?;
88            Ok(json_value)
89        }
90        None => {
91            Err(internal_error(format!(
92                "Unable to sample value with line number {} for file at path {}",
93                line_num, filename
94            )))
95        },
96    }
97}
98
99// Read the entire file in and store the individual lines if we haven't seen it before.
100// Otherwise, return the existing lines.
101fn read_all_file_lines<'a>(filepath: String) -> Result<Ref<'a, String, Vec<String>>> {
102    if !FILE_CACHE.contains_key(&filepath) {
103        let input_file: File =
104            File::open(&filepath).map_err(|source| read_file_error(filepath.clone(), source))?;
105        let buf_reader: BufReader<File> = BufReader::new(input_file);
106
107        let mut file_values: Vec<String> = Vec::new();
108        for line_result in buf_reader.lines() {
109            let line: String =
110                line_result.map_err(|source| read_file_error(filepath.clone(), source))?;
111            file_values.push(line);
112        }
113
114        if file_values.is_empty() {
115            return Err(empty_file(filepath));
116        }
117        FILE_CACHE.insert(filepath.clone(), file_values);
118    }
119    FILE_CACHE.get(&filepath)
120        .ok_or_else(|| internal_error(
121            format!("File cache did not contain an entry for file {filepath}")
122        ))
123}
124
125#[cfg(test)]
126mod tests {
127    use crate::common::tests::{test_tera_rand_function, test_tera_rand_function_returns_error};
128    use crate::file::*;
129    use tracing_test::traced_test;
130
131    #[test]
132    #[traced_test]
133    fn test_random_from_file() {
134        test_tera_rand_function(
135            random_from_file,
136            "random_from_file",
137            r#"{ "some_field": "{{ random_from_file(path="resources/test/days.txt") }}" }"#,
138            r#"\{ "some_field": "(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)" }"#,
139        )
140    }
141
142    #[test]
143    #[traced_test]
144    fn test_with_file_with_one_item() {
145        test_tera_rand_function(
146            random_from_file,
147            "random_from_file",
148            r#"{ "some_field": "{{ random_from_file(path="resources/test/file_with_one_item.txt") }}" }"#,
149            r#"\{ "some_field": "item" }"#,
150        )
151    }
152
153    #[test]
154    #[traced_test]
155    fn test_error_with_empty_file() {
156        test_tera_rand_function_returns_error(
157            random_from_file,
158            "random_from_file",
159            r#"{ "some_field": "{{ random_from_file(path="resources/test/empty_file.txt") }}" }"#,
160        )
161    }
162}