httpmock/
standalone.rs

1use std::fs;
2use std::fs::read_dir;
3use std::future::Future;
4use std::path::PathBuf;
5use std::process::Output;
6use std::str::FromStr;
7use std::sync::Arc;
8
9use regex::Regex;
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12use tokio::time::Duration;
13
14use crate::common::data::{MockDefinition, MockServerHttpResponse, Pattern, RequestRequirements};
15use crate::common::util::read_file;
16use crate::server::web::handlers::add_new_mock;
17use crate::server::{start_server, MockServerState};
18use crate::Method;
19
20#[derive(Debug, PartialEq, Serialize, Deserialize)]
21pub struct NameValuePair {
22    name: String,
23    value: String,
24}
25
26#[derive(Debug, Serialize, Deserialize)]
27struct YAMLRequestRequirements {
28    pub path: Option<String>,
29    pub path_contains: Option<Vec<String>>,
30    pub path_matches: Option<Vec<String>>,
31    pub method: Option<Method>,
32    pub header: Option<Vec<NameValuePair>>,
33    pub header_exists: Option<Vec<String>>,
34    pub cookie: Option<Vec<NameValuePair>>,
35    pub cookie_exists: Option<Vec<String>>,
36    pub body: Option<String>,
37    pub json_body: Option<Value>,
38    pub json_body_partial: Option<Vec<Value>>,
39    pub body_contains: Option<Vec<String>>,
40    pub body_matches: Option<Vec<String>>,
41    pub query_param_exists: Option<Vec<String>>,
42    pub query_param: Option<Vec<NameValuePair>>,
43    pub x_www_form_urlencoded_key_exists: Option<Vec<String>>,
44    pub x_www_form_urlencoded_tuple: Option<Vec<NameValuePair>>,
45}
46
47#[derive(Debug, Serialize, Deserialize)]
48struct YAMLHTTPResponse {
49    pub status: Option<u16>,
50    pub header: Option<Vec<NameValuePair>>,
51    pub body: Option<String>,
52    pub delay: Option<u64>,
53}
54
55#[derive(Debug, Serialize, Deserialize)]
56struct YAMLMockDefinition {
57    when: YAMLRequestRequirements,
58    then: YAMLHTTPResponse,
59}
60
61pub async fn start_standalone_server<F>(
62    port: u16,
63    expose: bool,
64    static_mock_dir_path: Option<PathBuf>,
65    print_access_log: bool,
66    history_limit: usize,
67    shutdown: F,
68) -> Result<(), String>
69where
70    F: Future<Output = ()>,
71{
72    let state = Arc::new(MockServerState::new(history_limit));
73
74    #[cfg(feature = "standalone")]
75    static_mock_dir_path.map(|path| {
76        read_static_mocks(path)
77            .into_iter()
78            .map(|d| map_to_mock_definition(d))
79            .for_each(|static_mock| {
80                add_new_mock(&state, static_mock, true).expect("cannot add static mock");
81            })
82    });
83
84    start_server(port, expose, &state, None, print_access_log, shutdown).await
85}
86
87#[cfg(feature = "standalone")]
88fn read_static_mocks(path: PathBuf) -> Vec<YAMLMockDefinition> {
89    let mut definitions = Vec::new();
90
91    let paths = read_dir(path).expect("cannot list files in directory");
92    for file_path in paths {
93        let file_path = file_path.unwrap().path();
94        if let Some(ext) = file_path.extension() {
95            if !"yaml".eq(ext) && !"yml".eq(ext) {
96                continue;
97            }
98        }
99
100        log::info!(
101            "Loading static mock file from '{}'",
102            file_path.to_string_lossy()
103        );
104        let content = read_file(file_path).expect("cannot read from file");
105        let content = String::from_utf8(content).expect("cannot convert file content");
106
107        definitions.push(serde_yaml::from_str(&content).unwrap());
108    }
109
110    return definitions;
111}
112
113#[cfg(feature = "standalone")]
114fn map_to_mock_definition(yaml_definition: YAMLMockDefinition) -> MockDefinition {
115    MockDefinition {
116        request: RequestRequirements {
117            path: yaml_definition.when.path,
118            path_contains: yaml_definition.when.path_contains,
119            path_matches: to_pattern_vec(yaml_definition.when.path_matches),
120            method: yaml_definition.when.method.map(|m| m.to_string()),
121            headers: to_pair_vec(yaml_definition.when.header),
122            header_exists: yaml_definition.when.header_exists,
123            cookies: to_pair_vec(yaml_definition.when.cookie),
124            cookie_exists: yaml_definition.when.cookie_exists,
125            body: yaml_definition.when.body,
126            json_body: yaml_definition.when.json_body,
127            json_body_includes: yaml_definition.when.json_body_partial,
128            body_contains: yaml_definition.when.body_contains,
129            body_matches: to_pattern_vec(yaml_definition.when.body_matches),
130            query_param_exists: yaml_definition.when.query_param_exists,
131            query_param: to_pair_vec(yaml_definition.when.query_param),
132            x_www_form_urlencoded: to_pair_vec(yaml_definition.when.x_www_form_urlencoded_tuple),
133            x_www_form_urlencoded_key_exists: yaml_definition.when.x_www_form_urlencoded_key_exists,
134            matchers: None,
135        },
136        response: MockServerHttpResponse {
137            status: yaml_definition.then.status,
138            headers: to_pair_vec(yaml_definition.then.header),
139            body: yaml_definition.then.body.map(|b| b.into_bytes()),
140            delay: yaml_definition.then.delay.map(|v| Duration::from_millis(v)),
141        },
142    }
143}
144
145#[cfg(feature = "standalone")]
146fn to_pattern_vec(vec: Option<Vec<String>>) -> Option<Vec<Pattern>> {
147    vec.map(|vec| {
148        vec.iter()
149            .map(|val| Pattern::from_regex(Regex::from_str(val).expect("cannot parse regex")))
150            .collect()
151    })
152}
153
154#[cfg(feature = "standalone")]
155fn to_pair_vec(kvp: Option<Vec<NameValuePair>>) -> Option<Vec<(String, String)>> {
156    kvp.map(|vec| vec.into_iter().map(|nvp| (nvp.name, nvp.value)).collect())
157}