1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
use std::fs;
use std::fs::read_dir;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;

use regex::Regex;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio::time::Duration;

use crate::common::data::{MockDefinition, MockServerHttpResponse, Pattern, RequestRequirements};
use crate::common::util::read_file;
use crate::server::web::handlers::add_new_mock;
use crate::server::{start_server, MockServerState};
use crate::Method;

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct NameValuePair {
    name: String,
    value: String,
}

#[derive(Debug, Serialize, Deserialize)]
struct YAMLRequestRequirements {
    pub path: Option<String>,
    pub path_contains: Option<Vec<String>>,
    pub path_matches: Option<Vec<String>>,
    pub method: Option<Method>,
    pub header: Option<Vec<NameValuePair>>,
    pub header_exists: Option<Vec<String>>,
    pub cookie: Option<Vec<NameValuePair>>,
    pub cookie_exists: Option<Vec<String>>,
    pub body: Option<String>,
    pub json_body: Option<Value>,
    pub json_body_partial: Option<Vec<Value>>,
    pub body_contains: Option<Vec<String>>,
    pub body_matches: Option<Vec<String>>,
    pub query_param_exists: Option<Vec<String>>,
    pub query_param: Option<Vec<NameValuePair>>,
    pub x_www_form_urlencoded_key_exists: Option<Vec<String>>,
    pub x_www_form_urlencoded_tuple: Option<Vec<NameValuePair>>,
}

#[derive(Debug, Serialize, Deserialize)]
struct YAMLHTTPResponse {
    pub status: Option<u16>,
    pub header: Option<Vec<NameValuePair>>,
    pub body: Option<String>,
    pub delay: Option<u64>,
}

#[derive(Debug, Serialize, Deserialize)]
struct YAMLMockDefinition {
    when: YAMLRequestRequirements,
    then: YAMLHTTPResponse,
}

pub async fn start_standalone_server(
    port: u16,
    expose: bool,
    static_mock_dir_path: Option<PathBuf>,
    print_access_log: bool,
    history_limit: usize,
) -> Result<(), String> {
    let state = Arc::new(MockServerState::new(history_limit));

    #[cfg(feature = "standalone")]
    static_mock_dir_path.map(|path| {
        read_static_mocks(path)
            .into_iter()
            .map(|d| map_to_mock_definition(d))
            .for_each(|static_mock| {
                add_new_mock(&state, static_mock, true).expect("cannot add static mock");
            })
    });

    start_server(port, expose, &state, None, print_access_log).await
}

#[cfg(feature = "standalone")]
fn read_static_mocks(path: PathBuf) -> Vec<YAMLMockDefinition> {
    let mut definitions = Vec::new();

    let paths = read_dir(path).expect("cannot list files in directory");
    for file_path in paths {
        let file_path = file_path.unwrap().path();
        if let Some(ext) = file_path.extension() {
            if !"yaml".eq(ext) && !"yml".eq(ext) {
                continue;
            }
        }

        log::info!(
            "Loading static mock file from '{}'",
            file_path.to_string_lossy()
        );
        let content = read_file(file_path).expect("cannot read from file");
        let content = String::from_utf8(content).expect("cannot convert file content");

        definitions.push(serde_yaml::from_str(&content).unwrap());
    }

    return definitions;
}

#[cfg(feature = "standalone")]
fn map_to_mock_definition(yaml_definition: YAMLMockDefinition) -> MockDefinition {
    MockDefinition {
        request: RequestRequirements {
            path: yaml_definition.when.path,
            path_contains: yaml_definition.when.path_contains,
            path_matches: to_pattern_vec(yaml_definition.when.path_matches),
            method: yaml_definition.when.method.map(|m| m.to_string()),
            headers: to_pair_vec(yaml_definition.when.header),
            header_exists: yaml_definition.when.header_exists,
            cookies: to_pair_vec(yaml_definition.when.cookie),
            cookie_exists: yaml_definition.when.cookie_exists,
            body: yaml_definition.when.body,
            json_body: yaml_definition.when.json_body,
            json_body_includes: yaml_definition.when.json_body_partial,
            body_contains: yaml_definition.when.body_contains,
            body_matches: to_pattern_vec(yaml_definition.when.body_matches),
            query_param_exists: yaml_definition.when.query_param_exists,
            query_param: to_pair_vec(yaml_definition.when.query_param),
            x_www_form_urlencoded: to_pair_vec(yaml_definition.when.x_www_form_urlencoded_tuple),
            x_www_form_urlencoded_key_exists: yaml_definition.when.x_www_form_urlencoded_key_exists,
            matchers: None,
        },
        response: MockServerHttpResponse {
            status: yaml_definition.then.status,
            headers: to_pair_vec(yaml_definition.then.header),
            body: yaml_definition.then.body.map(|b| b.into_bytes()),
            delay: yaml_definition.then.delay.map(|v| Duration::from_millis(v)),
        },
    }
}

#[cfg(feature = "standalone")]
fn to_pattern_vec(vec: Option<Vec<String>>) -> Option<Vec<Pattern>> {
    vec.map(|vec| {
        vec.iter()
            .map(|val| Pattern::from_regex(Regex::from_str(val).expect("cannot parse regex")))
            .collect()
    })
}

#[cfg(feature = "standalone")]
fn to_pair_vec(kvp: Option<Vec<NameValuePair>>) -> Option<Vec<(String, String)>> {
    kvp.map(|vec| vec.into_iter().map(|nvp| (nvp.name, nvp.value)).collect())
}