plantuml_server_client_rs/config/
file.rs

1use crate::config::ConfigTrait;
2use crate::{Format, MetadataVersion, Method};
3use anyhow::Context as _;
4use dirs::home_dir;
5use serde::Deserialize;
6use std::path::PathBuf;
7use tokio::fs::read_to_string;
8
9/// The default configuration file path list
10const PATHS: [&str; 2] = [".pscr.conf", ".config/.pscr.conf"];
11
12/// A representation of configuration from file
13///
14/// # Examples:
15///
16/// ```toml
17#[doc = include_str!("../../tests/assets/config/.pscr.conf")]
18/// ```
19#[derive(Deserialize, Clone, Debug)]
20#[serde(rename_all = "kebab-case")]
21pub struct FileConfig {
22    pscr: Config,
23}
24
25#[derive(Deserialize, Clone, Debug)]
26#[serde(rename_all = "kebab-case")]
27struct Config {
28    #[serde(default, deserialize_with = "de::deserialize_option_from_str")]
29    method: Option<Method>,
30
31    #[serde(default)]
32    url_prefix: Option<String>,
33
34    #[serde(default, deserialize_with = "de::deserialize_option_from_str")]
35    format: Option<Format>,
36
37    #[serde(default)]
38    combined: Option<bool>,
39
40    #[serde(default)]
41    metadata_version: Option<MetadataVersion>,
42}
43
44impl FileConfig {
45    /// Reads [`FileConfig`] from specified path.
46    ///
47    /// * `filepath` - A configuration file path
48    ///
49    /// # Examples
50    ///
51    /// ```
52    /// # use plantuml_server_client_rs as pscr;
53    /// #
54    /// # use std::path::PathBuf;
55    /// # use pscr::FileConfig;
56    /// # #[tokio::main]
57    /// async fn main() -> anyhow::Result<()> {
58    ///     let filepath: PathBuf = "./tests/assets/config/.pscr.conf".into();
59    ///     let _config = FileConfig::read_from_file(&filepath).await?;
60    ///     Ok(())
61    /// }
62    /// ```
63    pub async fn read_from_file(filepath: &PathBuf) -> anyhow::Result<Self> {
64        let data = read_to_string(filepath)
65            .await
66            .map_err(|e| {
67                tracing::warn!("failed to read '{filepath:?}': {e:?}");
68                e
69            })
70            .with_context(|| format!("failed to read '{filepath:?}'"))?;
71
72        toml::from_str(&data)
73            .map_err(|e| {
74                tracing::warn!("failed to parse '{filepath:?}': {e:?}");
75                e
76            })
77            .with_context(|| format!("failed to parse '{filepath:?}'"))
78    }
79
80    /// Reads the configuration from file in order of default path list.
81    ///
82    /// # Priority:
83    ///
84    /// 1. (out of function) specified by `--config` option (If `--config` specified and cannot read the file, the program exit by error for security.)
85    /// 1. `".pscr.conf"`
86    /// 1. `".config/.pscr.conf"`
87    /// 1. `"${HOME}/.pscr.conf"`
88    /// 1. `"${HOME}/.config/.pscr.conf"`
89    pub async fn read_from_default_path() -> Option<Self> {
90        let paths = Self::default_path_list();
91
92        for filepath in paths.into_iter() {
93            match Self::read_from_file(&filepath).await {
94                Ok(config) => {
95                    return Some(config);
96                }
97                Err(_) => {
98                    continue;
99                }
100            }
101        }
102
103        None
104    }
105
106    fn default_path_list() -> Vec<PathBuf> {
107        let prefixs: Vec<_> = [PathBuf::from(".")].into_iter().chain(home_dir()).collect();
108        let mut ret = vec![];
109
110        for prefix in prefixs.into_iter() {
111            for basepath in PATHS.iter() {
112                let mut filepath = prefix.clone();
113                filepath.push(basepath);
114                ret.push(filepath);
115            }
116        }
117
118        ret
119    }
120}
121
122impl ConfigTrait for FileConfig {
123    /// Returns HTTP Request Method ([`Method::Get`] or [`Method::Post`])
124    fn method(&self) -> Option<Method> {
125        self.pscr.method.clone()
126    }
127
128    /// Returns URL prefix of the PlantUML Server (e.g. `http://localhost:8080/`)
129    fn url_prefix(&self) -> Option<String> {
130        self.pscr.url_prefix.clone()
131    }
132
133    /// Returns output format ([`Format::Svg`] or [`Format::Png`] or [`Format::Ascii`] (`txt`))
134    fn format(&self) -> Option<Format> {
135        self.pscr.format.clone()
136    }
137
138    /// Returns whether to output a `combined` diagram.
139    fn combined(&self) -> Option<bool> {
140        self.pscr.combined
141    }
142
143    /// Returns whether to output a `combined` diagram.
144    fn metadata_version(&self) -> Option<MetadataVersion> {
145        self.pscr.metadata_version
146    }
147}
148
149mod de {
150    use serde::de::{Deserialize, Deserializer, Error as DeError};
151    use std::str::FromStr;
152
153    pub fn deserialize_option_from_str<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
154    where
155        D: Deserializer<'de>,
156        T: FromStr,
157        <T as FromStr>::Err: std::fmt::Display,
158    {
159        Option::<String>::deserialize(deserializer)?
160            .map(|s| T::from_str(&s).map_err(DeError::custom))
161            .transpose()
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    use itertools::Itertools;
170
171    #[tokio::test]
172    async fn test_de() -> anyhow::Result<()> {
173        let method_list = [
174            ("", None),
175            (r#"method = "post""#, Some(Method::Post)),
176            (r#"method = "get""#, Some(Method::Get)),
177        ];
178
179        let url_prefix_list = [
180            ("", None),
181            (
182                r#"url-prefix = "http://localhost:8080/""#,
183                Some("http://localhost:8080/"),
184            ),
185        ];
186
187        let format_list = [
188            ("", None),
189            (r#"format = "svg""#, Some(Format::Svg)),
190            (r#"format = "png""#, Some(Format::Png)),
191            (r#"format = "txt""#, Some(Format::Ascii)),
192        ];
193
194        let combined_list = [
195            ("", None),
196            (r#"combined = false"#, Some(false)),
197            (r#"combined = true"#, Some(true)),
198        ];
199
200        let metadata_version_list = [
201            ("", None),
202            (r#"metadata-version = "v2""#, Some(MetadataVersion::V2)),
203        ];
204
205        for ((((method, url_prefix), format), combined), metadata_version) in method_list
206            .iter()
207            .cartesian_product(url_prefix_list)
208            .cartesian_product(format_list)
209            .cartesian_product(combined_list)
210            .cartesian_product(metadata_version_list)
211        {
212            let (method_line, expected_method) = method;
213            let (url_prefix_line, expected_url_prefix) = url_prefix;
214            let (format_line, expected_format) = format;
215            let (combined_line, expected_combined) = combined;
216            let (metadata_version_line, expected_metadata_format_version) = metadata_version;
217
218            let testdata = format!(
219                r#"
220                [pscr]
221                {method_line}
222                {url_prefix_line}
223                {format_line}
224                {combined_line}
225                {metadata_version_line}
226                "#
227            );
228
229            println!("testdata: {testdata}");
230            let config: FileConfig = toml::from_str(&testdata)?;
231
232            assert_eq!(expected_method, &config.method());
233            assert_eq!(expected_url_prefix, config.url_prefix().as_deref());
234            assert_eq!(expected_format, config.format());
235            assert_eq!(expected_combined, config.combined());
236            assert_eq!(expected_metadata_format_version, config.metadata_version());
237        }
238
239        Ok(())
240    }
241
242    #[tokio::test]
243    async fn test_default_path_list() -> anyhow::Result<()> {
244        let paths = FileConfig::default_path_list();
245
246        assert_eq!(paths[0], PathBuf::from("./.pscr.conf"));
247        assert_eq!(paths[1], PathBuf::from("./.config/.pscr.conf"));
248
249        if let Some(homedir) = home_dir() {
250            let home = homedir.to_str().unwrap();
251
252            let path: PathBuf = [home, ".pscr.conf"].iter().collect();
253            assert_eq!(paths[2], path);
254
255            let path: PathBuf = [home, ".config", ".pscr.conf"].iter().collect();
256            assert_eq!(paths[3], path);
257        }
258
259        Ok(())
260    }
261}