fhttp_core/request_sources/
variable_support.rs

1use 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}