wwsvc_mock/
app_config.rs

1use std::{collections::HashMap, fmt::Display, path::Path, str::FromStr};
2
3use figment::{
4    providers::{Env, Format, Toml},
5    Figment,
6};
7use serde::Deserialize;
8use serde_inline_default::serde_inline_default;
9
10use crate::OptionalJson;
11
12fn generate_hash() -> String {
13    use rand::Rng;
14    let mut rng = rand::thread_rng();
15    let mut hash = String::new();
16    for _ in 0..32 {
17        hash.push_str(&format!("{:x}", rng.gen_range(0..16)));
18    }
19    hash
20}
21
22/// The main configuration of the mock server. See each field for more information.
23#[derive(Deserialize, Default, Debug, Clone)]
24pub struct AppConfig {
25    /// The server configuration, see [ServerConfig] for more information.
26    pub server: Option<ServerConfig>,
27    /// The webware mocking configuration, see [WebwareConfig] for more information.
28    #[serde(default = "WebwareConfig::default")]
29    pub webware: WebwareConfig,
30    /// A list of mock resources to be used by the server. For more information see [MockResource].
31    #[serde(default)]
32    pub mock_resources: Vec<MockResource>,
33    /// Whether to enable the debug middleware for logging requests and responses.
34    #[serde(default)]
35    pub debug: bool,
36}
37
38impl AppConfig {
39    /// Loads the configuration from both the `config.toml` file and the environment variables.
40    /// 
41    /// The environment variables are prefixed with `APP__` and split by `__`. For example, the
42    /// `server.bind_address` field can be set by the `APP__SERVER__BIND_ADDRESS` environment.
43    pub fn new() -> Result<Self, figment::Error> {
44        Figment::new()
45            .merge(Toml::file("config.toml"))
46            .merge(Env::prefixed("APP__").split("__"))
47            .extract()
48    }
49
50    /// Loads the configuration from the specified file and the environment variables.
51    ///
52    /// The environment variables are prefixed with `APP__` and split by `__`. For example, the
53    /// `server.bind_address` field can be set by the `APP__SERVER__BIND_ADDRESS` environment.
54    pub fn from_file(file: &Path) -> Result<Self, figment::Error> {
55        Figment::new()
56            .merge(Toml::file(file))
57            .merge(Env::prefixed("APP__").split("__"))
58            .extract()
59    }
60
61    /// Adds a [mock resource][MockResource] to the configuration.
62    pub fn with_mock_resource(mut self, resource: MockResource) -> Self {
63        self.mock_resources.push(resource);
64        self
65    }
66}
67
68/// The server configuration. This config only applies for the binary, not the library.
69#[derive(Deserialize, Debug, Clone)]
70pub struct ServerConfig {
71    /// The address to bind the server to. For example, `127.0.0.1:3000`.
72    pub bind_address: String,
73}
74
75/// The mocking configuration for the WEBWARE, which includes the webservices and the associated credentials.
76#[derive(Deserialize, Default, Debug, Clone)]
77pub struct WebwareConfig {
78    /// The configuration for the webservices, see [WebservicesConfig] for more information.
79    #[serde(default)]
80    pub webservices: WebservicesConfig,
81    /// The credentials that the webservices will accept. See [CredentialsConfig] for more information.
82    #[serde(default)]
83    pub credentials: CredentialsConfig,
84}
85
86/// The credentials configuration for the webservices.
87#[derive(Deserialize, Debug, Clone)]
88pub struct CredentialsConfig {
89    /// The service pass that the webservices will accept.
90    /// 
91    /// If not provided, a random 32 character hash will be generated.
92    #[serde(default = "generate_hash")]
93    pub service_pass: String,
94    /// The application ID that the webservices will accept.
95    /// 
96    /// If not provided, a random 32 character hash will be generated.
97    #[serde(default = "generate_hash")]
98    pub application_id: String,
99}
100
101impl Default for CredentialsConfig {
102    fn default() -> Self {
103        CredentialsConfig {
104            service_pass: generate_hash(),
105            application_id: generate_hash(),
106        }
107    }
108}
109
110/// The configuration for the webservices. You can either provide your own hashes or let the server generate them.
111#[serde_inline_default]
112#[derive(Deserialize, Debug, Clone)]
113pub struct WebservicesConfig {
114    /// The vendor hash that the webservices will accept.
115    /// 
116    /// If not provided, a random 32 character hash will be generated.
117    #[serde(default = "generate_hash")]
118    pub vendor_hash: String,
119    /// The application hash that the webservices will accept.
120    /// 
121    /// If not provided, a random 32 character hash will be generated.
122    #[serde(default = "generate_hash")]
123    pub application_hash: String,
124    /// The version of the webservices application that the server will accept.
125    /// 
126    /// If not provided, the version will be set to `1`.
127    #[serde_inline_default(1)]
128    pub version: u32,
129    /// The application secret that the webservices will accept.
130    /// 
131    /// If not provided, the secret will be set to `1`.
132    #[serde_inline_default("1".to_string())]
133    pub application_secret: String,
134}
135
136impl Default for WebservicesConfig {
137    fn default() -> Self {
138        WebservicesConfig {
139            vendor_hash: generate_hash(),
140            application_hash: generate_hash(),
141            version: 1,
142            application_secret: "1".to_string(),
143        }
144    }
145}
146
147/// A data source that can be either a file path, a string or empty.
148#[derive(Deserialize, Debug, Clone)]
149#[serde(tag = "type")]
150pub enum FileOrString {
151    /// A file path to read the data from.
152    File {
153        /// The path to the file.
154        file: String
155    },
156    /// A string to use as the data.
157    String {
158        /// The string value.
159        value: String
160    },
161    /// An empty data source.
162    Empty,
163}
164
165impl FileOrString {
166    /// Returns the data source as a string.
167    /// 
168    /// If the data source is a file, it will read the file and return the contents.
169    /// If the data source is a string, it will return the string.
170    /// If the data source is empty, it will return an empty string.
171    pub fn as_string(&self) -> String {
172        match self {
173            FileOrString::File { file } => std::fs::read_to_string(file).unwrap(),
174            FileOrString::String { value } => value.clone(),
175            FileOrString::Empty => "".to_string(),
176        }
177    }
178
179    /// Returns the data source as an [OptionalJson] value.
180    /// 
181    /// If the data source is a file, it will read the file and parse it as JSON.
182    /// If the data source is a string, it will parse the string as JSON.
183    /// If the data source is empty, it will return `None`.
184    pub fn as_json_value(&self) -> OptionalJson {
185        match self {
186            FileOrString::File { file: _ } => OptionalJson(Some(serde_json::from_str(&self.as_string()).unwrap())),
187            FileOrString::String { value: _ } => OptionalJson(Some(serde_json::from_str(&self.as_string()).unwrap())),
188            FileOrString::Empty => OptionalJson(None),
189        }
190    }
191}
192
193/// The method of the mock resource.
194/// 
195/// These are the methods that the WEBSERVICES accept for functions.
196#[derive(Deserialize, Debug, Clone, PartialEq)]
197pub enum MockResourceMethod {
198    /// The GET method, used for reading data.
199    /// 
200    /// Serializes and deserializes to and from `GET`.
201    #[serde(rename = "GET")]
202    Get,
203    /// The INSERT method, used for inserting data.
204    /// 
205    /// Serializes and deserializes to and from `INSERT`.
206    #[serde(rename = "INSERT")]
207    Insert,
208    /// The PUT method, used for updating data.
209    /// 
210    /// Serializes and deserializes to and from `PUT`.
211    #[serde(rename = "PUT")]
212    Put,
213    /// The DELETE method, used for deleting data.
214    /// 
215    /// Serializes and deserializes to and from `DELETE`.
216    #[serde(rename = "DELETE")]
217    Delete,
218    /// The EXEC method, used for executing functions.
219    /// 
220    /// Serializes and deserializes to and from `EXEC`.
221    #[serde(rename = "EXEC")]
222    Exec,
223}
224
225impl FromStr for MockResourceMethod {
226    type Err = String;
227
228    fn from_str(s: &str) -> Result<Self, Self::Err> {
229        match s {
230            "GET" => Ok(MockResourceMethod::Get),
231            "INSERT" => Ok(MockResourceMethod::Insert),
232            "PUT" => Ok(MockResourceMethod::Put),
233            "DELETE" => Ok(MockResourceMethod::Delete),
234            "EXEC" => Ok(MockResourceMethod::Exec),
235            _ => Err(format!("Unknown method: {}", s)),
236        }
237    }
238}
239
240impl Display for MockResourceMethod {
241    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
242        match self {
243            MockResourceMethod::Get => write!(f, "GET"),
244            MockResourceMethod::Insert => write!(f, "INSERT"),
245            MockResourceMethod::Put => write!(f, "PUT"),
246            MockResourceMethod::Delete => write!(f, "DELETE"),
247            MockResourceMethod::Exec => write!(f, "EXEC"),
248        }
249    }
250}
251
252/// A mock resource that the server will use to mock the WEBSERVICES.
253/// 
254/// The resource will only return the data from the data source if the function, method, revision and parameters match.
255/// There is currently no way to do wildcard matching.
256#[derive(Deserialize, Debug, Clone)]
257pub struct MockResource {
258    /// The [data source][FileOrString] for the mock resource.
259    pub data_source: FileOrString,
260    /// The function name for the mock resource.
261    /// 
262    /// This is the name of the function but without the method. For example, `ARTIKEL`.
263    pub function: String,
264    /// The method for the mock resource. See [MockResourceMethod] for more information.
265    pub method: MockResourceMethod,
266    /// The revision for the mock resource.
267    pub revision: u32,
268
269    /// The parameters for the mock resource.
270    pub parameters: Option<HashMap<String, String>>,
271}
272
273impl Display for MockResource {
274    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
275        write!(
276            f,
277            "MockResource {{ function: {}, method: {}, revision: {}, parameters: {} }}",
278            self.function, self.method, self.revision, match self.parameters {
279                Some(ref parameters) => serde_json::to_string(parameters).unwrap(),
280                None => "None".to_string(),
281            }
282        )
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use std::str::FromStr;
289
290    use pretty_assertions::assert_eq;
291
292    macro_rules! one_line_assert_eq {
293        ($fn:ident, $left:expr, $right:expr) => {
294            #[test]
295            fn $fn() {
296                assert_eq!($left, $right);
297            }
298        };
299    }
300
301    #[test]
302    fn default_config() {
303        let config = super::AppConfig::new().unwrap();
304        assert_eq!(config.mock_resources.is_empty(), true);
305        assert_eq!(config.debug, false);
306        assert_eq!(config.webware.credentials.service_pass.len(), 32);
307        assert_eq!(config.webware.credentials.application_id.len(), 32);
308        assert_eq!(config.webware.webservices.vendor_hash.len(), 32);
309        assert_eq!(config.webware.webservices.application_hash.len(), 32);
310        assert_eq!(config.webware.webservices.version, 1);
311        assert_eq!(config.webware.webservices.application_secret, "1".to_string());
312    }
313
314    #[test]
315    fn config_from_file() {
316        figment::Jail::expect_with(|jail| {
317            jail.create_file("test-config.toml", r#"[server]
318            bind_address = "0.0.0.0:3000"
319            
320            [[mock_resources]]
321            data_source.type = "Empty"
322            function = "ARTIKEL"
323            method = "INSERT"
324            revision = 1
325            parameters.ARTNR = "MeinArtikel""#)?;
326
327            let config = super::AppConfig::from_file(std::path::Path::new("test-config.toml")).unwrap();
328            assert_eq!(config.server.unwrap().bind_address, "0.0.0.0:3000");
329            assert_eq!(config.mock_resources.len(), 1);
330            assert_eq!(config.mock_resources[0].function, "ARTIKEL");
331            assert_eq!(config.mock_resources[0].method, super::MockResourceMethod::Insert);
332            assert_eq!(config.mock_resources[0].revision, 1);
333            assert_eq!(config.mock_resources[0].parameters.as_ref().unwrap().get("ARTNR").unwrap(), "MeinArtikel");
334
335            Ok(())
336        });
337    }
338
339    one_line_assert_eq!(method_get_to_string, super::MockResourceMethod::Get.to_string(), "GET");
340    one_line_assert_eq!(method_insert_to_string, super::MockResourceMethod::Insert.to_string(), "INSERT");
341    one_line_assert_eq!(method_put_to_string, super::MockResourceMethod::Put.to_string(), "PUT");
342    one_line_assert_eq!(method_delete_to_string, super::MockResourceMethod::Delete.to_string(), "DELETE");
343    one_line_assert_eq!(method_exec_to_string, super::MockResourceMethod::Exec.to_string(), "EXEC");
344    one_line_assert_eq!(mock_resource_without_params_to_string, super::MockResource {
345        data_source: super::FileOrString::File {
346            file: "data/artikel_clean.json".to_string(),
347        },
348        function: "ARTIKEL".to_string(),
349        method: super::MockResourceMethod::Get,
350        revision: 3,
351        parameters: None,
352    }.to_string(), "MockResource { function: ARTIKEL, method: GET, revision: 3, parameters: None }");
353    one_line_assert_eq!(mock_resource_with_params_to_string, super::MockResource {
354        data_source: super::FileOrString::File {
355            file: "data/artikel_art_nr_clean.json".to_string(),
356        },
357        function: "ARTIKEL".to_string(),
358        method: super::MockResourceMethod::Get,
359        revision: 3,
360        parameters: Some(wwsvc_rs::collection! {
361            "FELDER".to_string() => "ART_1_25".to_string(),
362        })
363    }.to_string(), "MockResource { function: ARTIKEL, method: GET, revision: 3, parameters: {\"FELDER\":\"ART_1_25\"} }");
364    one_line_assert_eq!(unknown_method_from_str, super::MockResourceMethod::from_str("UNKNOWN").unwrap_err(), "Unknown method: UNKNOWN");
365    one_line_assert_eq!(empty_as_str, super::FileOrString::Empty.as_string(), "");
366}