fhttp_core/request_sources/
variable_support.rs1use std::path::Path;
2
3use anyhow::Result;
4use regex::Captures;
5use uuid::Uuid;
6
7use crate::path_utils::get_dependency_path;
8use crate::preprocessing::dependant::request_dependencies;
9use crate::preprocessing::evaluation::{BaseEvaluation, Evaluation};
10use crate::preprocessing::random_numbers::{parse_min_max, random_int, RandomNumberEval};
11use crate::{Config, Profile, ResponseStore};
12
13#[derive(Debug)]
14pub struct EnvVarOccurrence<'a> {
15 pub name: &'a str,
16 pub default: Option<&'a str>,
17 pub base_evaluation: BaseEvaluation,
18}
19
20impl AsRef<BaseEvaluation> for EnvVarOccurrence<'_> {
21 fn as_ref(&self) -> &BaseEvaluation {
22 &self.base_evaluation
23 }
24}
25
26pub fn replace_evals(
27 text: String,
28 base_path: impl AsRef<Path>,
29 dependency: bool,
30 profile: &Profile,
31 config: &Config,
32 response_store: &ResponseStore,
33) -> Result<String> {
34 let text = replace_env_vars(text, dependency, profile, config, response_store)?;
35 let text = replace_uuids(text);
36 let text = replace_random_ints(text)?;
37 let text = replace_request_dependencies(text, base_path, response_store)?;
38 Ok(text)
39}
40
41pub fn get_env_vars(text: &str) -> Vec<EnvVarOccurrence> {
42 let re_env = regex!(r##"(?m)(\\*)(\$\{env\(([a-zA-Z0-9-_]+)(\s*,\s*"([^"]*)")?\)})"##);
43
44 re_env
45 .captures_iter(text)
46 .collect::<Vec<Captures>>()
47 .into_iter()
48 .rev()
49 .map(|capture: Captures| {
50 let backslashes = capture.get(1).unwrap().range();
51 let group = capture.get(2).unwrap();
52 let key = capture.get(3).unwrap().as_str();
53 let default = capture.get(5).map(|m| m.as_str());
54 EnvVarOccurrence {
55 name: key,
56 default,
57 base_evaluation: BaseEvaluation {
58 range: group.range(),
59 backslashes,
60 },
61 }
62 })
63 .collect()
64}
65
66fn replace_env_vars(
67 text: String,
68 dependency: bool,
69 profile: &Profile,
70 config: &Config,
71 response_store: &ResponseStore,
72) -> Result<String> {
73 let variables = get_env_vars(&text);
74
75 if variables.is_empty() {
76 Ok(text)
77 } else {
78 let mut buffer = text.clone();
79 for occurrence in variables {
80 occurrence.replace(&mut buffer, || {
81 profile.get(
82 occurrence.name,
83 config,
84 response_store,
85 occurrence.default,
86 dependency,
87 )
88 })?;
89 }
90 Ok(buffer)
91 }
92}
93
94fn replace_uuids(text: String) -> String {
95 let re_env = regex!(r"(?m)(\\*)(\$\{uuid\(\)})");
96
97 let reversed_evaluations: Vec<BaseEvaluation> = re_env
98 .captures_iter(&text)
99 .collect::<Vec<_>>()
100 .into_iter()
101 .rev()
102 .map(|capture: Captures| {
103 let backslashes = capture.get(1).unwrap().range();
104 let group = capture.get(2).unwrap();
105 BaseEvaluation::new(group.range(), backslashes)
106 })
107 .collect();
108
109 if reversed_evaluations.is_empty() {
110 text
111 } else {
112 let mut buffer = text.clone();
113
114 for eval in reversed_evaluations {
115 let _ = eval.replace(&mut buffer, || Ok(Uuid::new_v4().to_string()));
116 }
117
118 buffer
119 }
120}
121
122fn replace_random_ints(text: String) -> Result<String> {
123 let re_env = regex!(r"(?m)(\\*)(\$\{randomInt\(\s*([+-]?\d+)?\s*(,\s*([+-]?\d+)\s*)?\)})");
124
125 let reversed_random_nums: Vec<RandomNumberEval> = re_env
126 .captures_iter(&text)
127 .collect::<Vec<_>>()
128 .into_iter()
129 .rev()
130 .map(|capture: Captures| {
131 let backslashes = capture.get(1).unwrap().range();
132 let group = capture.get(2).unwrap();
133 let min = capture.get(3).map(|it| it.as_str());
134 let max = capture.get(5).map(|it| it.as_str());
135 let range = group.range();
136
137 RandomNumberEval::new(min, max, range, backslashes)
138 })
139 .collect();
140
141 if reversed_random_nums.is_empty() {
142 Ok(text)
143 } else {
144 let mut buffer = text.clone();
145
146 for eval in reversed_random_nums {
147 eval.replace(&mut buffer, || {
148 let (min, max) = parse_min_max(eval.min, eval.max)?;
149 Ok(random_int(min, max).to_string())
150 })?;
151 }
152
153 Ok(buffer)
154 }
155}
156
157fn replace_request_dependencies(
158 text: String,
159 base_path: impl AsRef<Path>,
160 response_store: &ResponseStore,
161) -> Result<String> {
162 let reversed_evals = request_dependencies(&text)?;
163
164 if reversed_evals.is_empty() {
165 Ok(text)
166 } else {
167 let mut buffer = text.clone();
168
169 for eval in reversed_evals {
170 eval.replace(&mut buffer, || {
171 Ok(response_store.get(&get_dependency_path(&base_path, eval.path)?))
172 })?;
173 }
174
175 Ok(buffer)
176 }
177}
178
179#[cfg(test)]
180mod replace_variables {
181 use std::env;
182
183 use indoc::indoc;
184
185 use crate::preprocessing::random_numbers::RANDOM_INT_CALLS;
186 use crate::test_utils::root;
187 use crate::RequestSource;
188
189 use super::*;
190
191 #[test]
192 fn should_replace_env_vars() -> Result<()> {
193 env::set_var("SERVER", "server");
194 env::set_var("TOKEN", "token");
195 env::set_var("BODY", "body");
196
197 let req = RequestSource::new(
198 env::current_dir().unwrap(),
199 indoc!(
200 r##"
201 GET http://${env(SERVER)}
202 Authorization: ${env(TOKEN)}
203
204 X${env(BODY)}X
205 "##
206 ),
207 )?;
208
209 let req = req.replace_variables(
210 &Profile::empty(env::current_dir().unwrap()),
211 &Config::default(),
212 &ResponseStore::new(),
213 )?;
214
215 assert_eq!(
216 &req.text,
217 indoc!(
218 r##"
219 GET http://server
220 Authorization: token
221
222 XbodyX
223 "##
224 )
225 );
226
227 Ok(())
228 }
229
230 #[test]
231 fn should_respect_backslashes_for_escaping_env_vars() -> Result<()> {
232 env::set_var("VAR", "X");
233
234 let req = RequestSource::new(
235 env::current_dir().unwrap(),
236 indoc!(
237 r##"
238 GET http://${env(VAR)}
239
240 \${env(VAR)}
241 \\${env(VAR)}
242 \\\${env(VAR)}
243 \\\\${env(VAR)}
244 "##
245 ),
246 )?;
247
248 let req = req.replace_variables(
249 &Profile::empty(env::current_dir().unwrap()),
250 &Config::default(),
251 &ResponseStore::new(),
252 )?;
253
254 assert_eq!(
255 &req.text,
256 indoc!(
257 r##"
258 GET http://X
259
260 ${env(VAR)}
261 \X
262 \${env(VAR)}
263 \\X
264 "##
265 )
266 );
267
268 Ok(())
269 }
270
271 #[test]
272 fn should_handle_env_var_default_values() -> Result<()> {
273 env::set_var("BODY", "body");
274
275 let req = RequestSource::new(
276 env::current_dir().unwrap(),
277 indoc!(
278 r##"
279 GET ${env(SRV, "http://localhost:8080")}
280
281 ${env(BODY, "default body")}
282 "##
283 ),
284 )?;
285
286 let req = req.replace_variables(
287 &Profile::empty(env::current_dir().unwrap()),
288 &Config::default(),
289 &ResponseStore::new(),
290 )?;
291
292 assert_eq!(
293 &req.text,
294 indoc!(
295 r##"
296 GET http://localhost:8080
297
298 body
299 "##
300 )
301 );
302
303 Ok(())
304 }
305
306 #[test]
307 fn should_replace_uuids() -> Result<()> {
308 let regex = regex!(r"X[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}X");
309
310 let req = RequestSource::new(
311 env::current_dir().unwrap(),
312 indoc!(
313 r##"
314 GET http://X${uuid()}X
315 "##
316 ),
317 )?;
318
319 let req = req.replace_variables(
320 &Profile::empty(env::current_dir().unwrap()),
321 &Config::default(),
322 &ResponseStore::new(),
323 )?;
324
325 assert!(regex.is_match(&req.text));
326
327 Ok(())
328 }
329
330 #[test]
331 fn should_respect_backslashes_replacing_uuids() -> Result<()> {
332 use regex::Regex;
333
334 let pattern = "[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}";
335 let uuid = "$\\{uuid\\(\\)\\}";
336 let format = format!(
337 "{p}\\n\\{u}\\n\\\\{p}\\n\\\\\\{u}\\n\\\\\\\\{p}",
338 p = pattern,
339 u = uuid
340 );
341 let regex = Regex::new(&format).unwrap();
342
343 let req = RequestSource::new(
344 env::current_dir().unwrap(),
345 indoc!(
346 r##"
347 GET http://server
348
349 ${uuid()}
350 \${uuid()}
351 \\${uuid()}
352 \\\${uuid()}
353 \\\\${uuid()}
354 "##
355 ),
356 )?;
357
358 let req = req.replace_variables(
359 &Profile::empty(env::current_dir().unwrap()),
360 &Config::default(),
361 &ResponseStore::new(),
362 )?;
363
364 assert!(regex.is_match(&req.text));
365
366 Ok(())
367 }
368
369 #[test]
370 fn should_replace_random_numbers() -> Result<()> {
371 RANDOM_INT_CALLS.with(|cell| {
372 cell.borrow_mut().clear();
373 });
374
375 let req = RequestSource::new(
376 env::current_dir().unwrap(),
377 indoc!(
378 r##"
379 GET http://server
380
381 ${randomInt()}
382 ${randomInt(-5)}
383 ${randomInt(-5, 7)}
384 "##
385 ),
386 )?;
387
388 req.replace_variables(
389 &Profile::empty(env::current_dir().unwrap()),
390 &Config::default(),
391 &ResponseStore::new(),
392 )?;
393
394 RANDOM_INT_CALLS.with(|calls| {
395 assert_eq!(
396 *calls.borrow(),
397 vec![(-5, 7), (-5, i32::MAX), (0, i32::MAX),]
398 );
399 });
400
401 Ok(())
402 }
403
404 #[test]
405 fn random_numbers_should_validate_params() -> Result<()> {
406 let profile = Profile::empty(env::current_dir().unwrap());
407 let config = Config::default();
408 let response_store = ResponseStore::new();
409
410 assert_err!(
411 RequestSource::new(
412 env::current_dir().unwrap(),
413 format!("GET ${{randomInt({})}}", i32::MIN as i64 - 1)
414 )?
415 .replace_variables(&profile, &config, &response_store),
416 format!("min param out of bounds: {}..{}", i32::MIN, i32::MAX)
417 );
418
419 assert_err!(
420 RequestSource::new(
421 env::current_dir().unwrap(),
422 format!("${{randomInt(0, {})}}", i32::MAX as i64 + 1)
423 )?
424 .replace_variables(&profile, &config, &response_store),
425 format!("max param out of bounds: {}..{}", i32::MIN, i32::MAX)
426 );
427
428 assert_err!(
429 RequestSource::new(env::current_dir().unwrap(), "${randomInt(3, 2)}")?
430 .replace_variables(&profile, &config, &response_store),
431 "min cannot be greater than max"
432 );
433
434 Ok(())
435 }
436
437 #[test]
438 fn replace_random_numbers_should_respect_backslashes() -> Result<()> {
439 let req = RequestSource::new(
440 env::current_dir().unwrap(),
441 indoc!(
442 r##"
443 GET http://server
444
445 ${randomInt()}
446 \${randomInt()}
447 \\${randomInt()}
448 \\\${randomInt()}
449 \\\\${randomInt()}
450 "##
451 ),
452 )?;
453
454 let req = req.replace_variables(
455 &Profile::empty(env::current_dir().unwrap()),
456 &Config::default(),
457 &ResponseStore::new(),
458 )?;
459
460 assert_eq!(
461 &req.text,
462 indoc!(
463 r##"
464 GET http://server
465
466 7
467 ${randomInt()}
468 \7
469 \${randomInt()}
470 \\7
471 "##
472 )
473 );
474
475 Ok(())
476 }
477
478 #[test]
479 fn should_replace_request_dependencies() -> Result<()> {
480 let path = root().join("resources/test/requests/dummy.http");
481 let profile = Profile::empty(env::current_dir().unwrap());
482 let config = Config::default();
483 let response_store = {
484 let mut tmp = ResponseStore::new();
485 tmp.store(path.clone(), "FOO");
486 tmp
487 };
488
489 let req = RequestSource::new(
490 env::current_dir().unwrap(),
491 indoc!(
492 r#"
493 GET server
494
495 ${request("../resources/test/requests/dummy.http")}
496 \${request("../resources/test/requests/dummy.http")}
497 \\${request("../resources/test/requests/dummy.http")}
498 \\\${request("../resources/test/requests/dummy.http")}
499 "#
500 ),
501 )?;
502 let req = req.replace_variables(&profile, &config, &response_store)?;
503
504 assert_eq!(
505 req.text,
506 indoc!(
507 r#"
508 GET server
509
510 FOO
511 ${request("../resources/test/requests/dummy.http")}
512 \FOO
513 \${request("../resources/test/requests/dummy.http")}
514 "#
515 )
516 );
517
518 Ok(())
519 }
520}