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
16pub 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
46pub 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
99fn 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}