Skip to main content

wireman_config/
config.rs

1#![allow(clippy::module_name_repetitions)]
2use crate::error::Error;
3use crate::error::Result;
4use crate::install::expand_file;
5use crate::install::expand_path;
6use logger::LogLevel;
7use serde::{Deserialize, Serialize};
8use std::fs::read_to_string;
9use std::path::Path;
10use std::str::FromStr;
11use theme::Config as ThemeConfig;
12
13/// The top level config.
14#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
15pub struct Config {
16    /// The include directories in which to search for the protos
17    pub includes: Vec<String>,
18    /// A list of proto files such as [internal.proto, api.proto]
19    pub files: Vec<String>,
20    /// The history config
21    #[serde(default)]
22    pub history: HistoryConfig,
23    /// The server config
24    #[serde(default)]
25    pub server: ServerConfig,
26    /// The logger config
27    #[serde(default)]
28    pub logging: LoggingConfig,
29    /// The ui config
30    #[serde(default)]
31    pub ui: ThemeConfig,
32    /// Optional TLS settings
33    #[serde(default)]
34    pub tls: TlsConfig,
35}
36
37impl Config {
38    /// Loads the config from a file.
39    ///
40    /// # Errors
41    ///
42    /// Failed to read the config file.
43    pub fn load(file: &str) -> Result<Self> {
44        let f = expand_file(file);
45        let data = read_to_string(&f).map_err(|err| Error::ReadConfigError {
46            filename: f,
47            source: err,
48        })?;
49        Self::deserialize_toml(&data)
50    }
51
52    /// Returns the content of the config as a string.
53    ///
54    /// # Errors
55    ///
56    /// Failed to read the config file.
57    pub fn read_to_string(file: &str) -> Result<String> {
58        let f = expand_file(file);
59        let data = read_to_string(&f).map_err(|err| Error::ReadConfigError {
60            filename: f,
61            source: err,
62        })?;
63        Ok(data)
64    }
65
66    /// Parses the config from a toml-formatted string.
67    ///
68    /// # Errors
69    ///
70    /// Returns an error if serde deserialization fails.
71    fn deserialize_toml(data: &str) -> Result<Self> {
72        toml::from_str(data).map_err(Error::DeserializeConfigError)
73    }
74
75    /// Serializes the config to a toml-formatted string.
76    ///
77    /// # Errors
78    ///
79    /// Returns an error if serde serialization fails.
80    pub fn serialize_toml(&self) -> Result<String> {
81        toml::to_string(self).map_err(Error::SerializeConfigError)
82    }
83
84    /// Gets the includes directories. Tries to shell expand the path
85    /// if it contains environment variables such as $HOME or ~.
86    #[must_use]
87    pub fn includes(&self) -> Vec<String> {
88        self.includes.iter().map(|e| expand_path(e)).collect()
89    }
90
91    /// Gets the files. Tries to shell expand the path if it contains
92    ///  environment variables such as $HOME or ~.
93    #[must_use]
94    pub fn files(&self) -> Vec<String> {
95        self.files.iter().map(|e| expand_file(e)).collect()
96    }
97}
98
99impl FromStr for Config {
100    type Err = Error;
101
102    /// Parses the config from a toml-formatted string.
103    ///
104    /// # Errors
105    ///
106    /// Returns an error if serde deserialization fails.
107    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
108        Self::deserialize_toml(s)
109    }
110}
111
112/// The config for the server values of the grpc client.
113#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq, PartialOrd)]
114pub struct ServerConfig {
115    /// The default address
116    pub default_address: Option<String>,
117    /// The default auth header
118    pub default_auth_header: Option<String>,
119}
120
121impl ServerConfig {
122    #[must_use]
123    pub fn new(default_address: &str, default_auth_header: &str) -> Self {
124        let default_address = if default_address.is_empty() {
125            None
126        } else {
127            Some(default_address.to_string())
128        };
129
130        let default_auth_header = if default_auth_header.is_empty() {
131            None
132        } else {
133            Some(default_auth_header.to_string())
134        };
135        Self {
136            default_address,
137            default_auth_header,
138        }
139    }
140}
141
142/// The history config of the grpc client.
143#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd)]
144pub struct HistoryConfig {
145    /// The directory where the history is saved
146    #[serde(default)]
147    pub directory: String,
148    /// Whether autosave should be enables
149    #[serde(default = "default_autosave")]
150    pub autosave: bool,
151    /// Whether the history is disabled
152    #[serde(default)]
153    pub disabled: bool,
154}
155
156impl Default for HistoryConfig {
157    fn default() -> Self {
158        Self {
159            directory: String::default(),
160            autosave: true,
161            disabled: false,
162        }
163    }
164}
165
166fn default_autosave() -> bool {
167    true
168}
169
170impl HistoryConfig {
171    /// Instantiate a new history config
172    #[must_use]
173    pub fn new(directory: &str, autosave: bool, disabled: bool) -> Self {
174        Self {
175            directory: directory.to_string(),
176            autosave,
177            disabled,
178        }
179    }
180
181    /// Returns the path to the history. Tries to shell expand the path if it contains
182    /// environment variables such as $HOME or ~.
183    #[must_use]
184    pub fn directory_expanded(&self) -> String {
185        if self.directory.is_empty() {
186            return String::new();
187        }
188        expand_path(&self.directory)
189    }
190}
191
192/// The logger config for wireman
193#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq, PartialOrd)]
194pub struct LoggingConfig {
195    /// The log level
196    #[serde(default)]
197    pub level: LogLevel,
198    /// The directory to where the log file should be stored
199    pub directory: String,
200}
201
202impl LoggingConfig {
203    /// Instantiate a new logging config
204    #[must_use]
205    pub fn new(level: LogLevel, file_path: &str) -> Self {
206        Self {
207            level,
208            directory: file_path.to_string(),
209        }
210    }
211
212    /// Returns the path to the directory of logger file. Tries to shell expand the path if it contains
213    /// environment variables such as $HOME or ~.
214    #[must_use]
215    pub fn directory_expanded(&self) -> String {
216        if self.directory.is_empty() {
217            return String::new();
218        }
219        expand_path(&self.directory)
220    }
221
222    /// Returns the path to the logger file. Tries to shell expand the path if it contains
223    /// environment variables such as $HOME or ~.
224    #[must_use]
225    pub fn file_path_expanded(&self) -> String {
226        let directory_expanded = self.directory_expanded();
227        let file_path = Path::new(&directory_expanded).join(Self::file_name());
228        file_path.to_string_lossy().to_string()
229    }
230
231    #[must_use]
232    pub(crate) fn file_name() -> String {
233        String::from("wireman.log")
234    }
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq, PartialOrd)]
238pub struct TlsConfig {
239    pub use_native: Option<bool>,
240    pub custom_cert: Option<String>,
241}
242
243impl TlsConfig {
244    #[must_use]
245    pub fn new(use_native: bool) -> Self {
246        Self {
247            use_native: Some(use_native),
248            custom_cert: None,
249        }
250    }
251
252    #[must_use]
253    pub fn custom(custom: &str) -> Self {
254        Self {
255            use_native: None,
256            custom_cert: Some(custom.to_string()),
257        }
258    }
259}
260
261#[cfg(test)]
262mod test {
263    use super::*;
264
265    #[test]
266    fn test_deserialize_toml() {
267        let data = r#"
268        includes = [
269            "/Users/myworkspace"
270        ]
271        files = [
272            "api.proto",
273            "internal.proto"
274        ]
275        [server]
276        default_address = "http://localhost:50051"
277        [history]
278        directory = "/Users/test"
279        autosave = false
280        [logging]
281        directory = "/Users"
282        level = "Debug"
283        [tls]
284        custom_cert = "cert.pem"
285        [ui]
286        skin = "skin.toml"
287        "#;
288        let cfg = Config::deserialize_toml(&data).unwrap();
289        let expected = Config {
290            includes: vec!["/Users/myworkspace".to_string()],
291            files: vec!["api.proto".to_string(), "internal.proto".to_string()],
292            tls: TlsConfig::custom("cert.pem"),
293            server: ServerConfig::new("http://localhost:50051", ""),
294            logging: LoggingConfig::new(LogLevel::Debug, "/Users"),
295            history: HistoryConfig::new("/Users/test", false, false),
296            ui: theme::Config::new(Some(String::from("skin.toml"))),
297        };
298        assert_eq!(cfg, expected);
299    }
300
301    #[test]
302    fn test_serialize_toml() {
303        let cfg = Config {
304            includes: vec!["/Users/myworkspace".to_string()],
305            files: vec!["api.proto".to_string(), "internal.proto".to_string()],
306            tls: TlsConfig::default(),
307            server: ServerConfig::new("http://localhost:50051", ""),
308            logging: LoggingConfig::new(LogLevel::Debug, "/Users"),
309            history: HistoryConfig::new("/Users/test", false, false),
310            ui: theme::Config::new(Some(String::from("skin.toml"))),
311        };
312        let expected = r#"includes = ["/Users/myworkspace"]
313files = ["api.proto", "internal.proto"]
314
315[history]
316directory = "/Users/test"
317autosave = false
318disabled = false
319
320[server]
321default_address = "http://localhost:50051"
322
323[logging]
324level = "Debug"
325directory = "/Users"
326
327[ui]
328skin = "skin.toml"
329
330[tls]
331"#;
332        assert_eq!(cfg.serialize_toml().unwrap(), expected);
333    }
334
335    #[test]
336    fn test_shell_expand() {
337        let cfg = Config {
338            includes: vec!["$HOME/workspace".to_string()],
339            files: vec![],
340            tls: TlsConfig::default(),
341            server: ServerConfig::default(),
342            logging: LoggingConfig::default(),
343            history: HistoryConfig::default(),
344            ui: ThemeConfig::default(),
345        };
346        let got = cfg.includes();
347        let home = std::env::var("HOME").unwrap();
348        assert_eq!(got.first(), Some(&format!("{home}/workspace")));
349    }
350}