runcc/config/read/
format.rs

1use serde::de::DeserializeOwned;
2use std::{fs, fs::File, io, path::Path, str::FromStr};
3
4use super::error::*;
5
6#[non_exhaustive]
7#[derive(Debug, Clone, Copy)]
8pub enum ConfigFormat {
9    Json,
10    Yaml,
11    Ron,
12    Toml,
13    /// See: [Cargo.toml metadata table](https://doc.rust-lang.org/cargo/reference/manifest.html#the-metadata-table)
14    CargoMetadata,
15}
16
17const EXTENSIONS: [(ConfigFormat, &str); 5] = [
18    (ConfigFormat::Json, ".json"),
19    (ConfigFormat::Yaml, ".yml"),
20    (ConfigFormat::Yaml, ".yaml"),
21    (ConfigFormat::Ron, ".ron"),
22    (ConfigFormat::Toml, ".toml"),
23];
24
25fn parse_str_with_format<T: DeserializeOwned>(
26    s: &str,
27    config_format: ConfigFormat,
28) -> Result<T, ConfigDeserializeErrorKind> {
29    let res: T = match config_format {
30        ConfigFormat::Json => serde_json::from_str(s)?,
31        ConfigFormat::Yaml => serde_yaml::from_str(s)?,
32        ConfigFormat::Ron => ron::from_str(s)?,
33        ConfigFormat::Toml => toml::from_str(s)?,
34        ConfigFormat::CargoMetadata => {
35            return Err(ConfigDeserializeErrorKind::CargoMetadataError(
36                CargoMetadataError::NoData,
37            ));
38        }
39    };
40    Ok(res)
41}
42
43pub enum ConfigFileContent {
44    File(File),
45}
46
47#[non_exhaustive]
48pub struct ConfigFileData<T> {
49    pub filename: String,
50    pub format: ConfigFormat,
51    pub data: T,
52}
53
54pub fn find_config_file_in_dir<T: DeserializeOwned>(
55    dir_path: &Path,
56    app_name: &str,
57) -> Result<ConfigFileData<T>, FindConfigError> {
58    let mut patterns = Vec::with_capacity(EXTENSIONS.len() + 1);
59    for (format, ext) in EXTENSIONS {
60        let filename = format!("{}{}", app_name, ext);
61        let file = dir_path.join(&filename);
62        patterns.push(filename);
63
64        match read_config_from_file_and_format(&file, format) {
65            Ok(conf) => return Ok(conf),
66            Err(err) => match err {
67                ReadConfigError::OpenFileError { error, .. }
68                    if error.kind() == io::ErrorKind::NotFound =>
69                {
70                    // file not found
71                }
72                err => return Err(FindConfigError::ReadError(err)),
73            },
74        };
75    }
76
77    match read_config_from_cargo_toml(&dir_path.join("Cargo.toml"), app_name) {
78        Ok(Some(conf)) => return Ok(conf),
79        Ok(None) => {
80            // read Cargo.toml but no field
81        }
82        Err(err) => {
83            match err {
84                ReadConfigError::OpenFileError { error, .. }
85                    if error.kind() == io::ErrorKind::NotFound =>
86                {
87                    // Cargo.toml not found
88                }
89                err => return Err(FindConfigError::ReadError(err)),
90            }
91        }
92    };
93
94    patterns.push("Cargo.toml".to_string());
95
96    let dir = dir_path.to_string_lossy();
97
98    let dir = if dir.is_empty() {
99        "current working directory".to_string()
100    } else {
101        dir.into_owned()
102    };
103
104    Err(FindConfigError::NoFileMatch { patterns, dir })
105}
106
107pub fn read_config_from_file_and_format<T: DeserializeOwned>(
108    file_path: &Path,
109    format: ConfigFormat,
110) -> Result<ConfigFileData<T>, ReadConfigError> {
111    let filename = file_path.to_string_lossy().into_owned();
112
113    match fs::read_to_string(file_path) {
114        Ok(s) => match parse_str_with_format(&s, format) {
115            Ok(data) => Ok(ConfigFileData {
116                filename,
117                format,
118                data,
119            }),
120            Err(kind) => Err(ConfigDeserializeError {
121                filename,
122                format,
123                kind,
124            }
125            .into()),
126        },
127        Err(error) => Err(ReadConfigError::OpenFileError {
128            error,
129            file: filename,
130        }),
131    }
132}
133
134pub fn read_config_from_cargo_toml<T: DeserializeOwned>(
135    file_path: &Path,
136    app_name: &str,
137) -> Result<Option<ConfigFileData<T>>, ReadConfigError> {
138    let filename = file_path.to_string_lossy().into_owned();
139    let format = ConfigFormat::CargoMetadata;
140
141    let s = match fs::read_to_string(file_path) {
142        Ok(s) => s,
143        Err(error) => {
144            return Err(ReadConfigError::OpenFileError {
145                file: filename,
146                error,
147            })
148        }
149    };
150
151    let v = match toml::Value::from_str(&s) {
152        Ok(v) => v,
153        Err(err) => {
154            return Err(ConfigDeserializeError {
155                filename,
156                format,
157                kind: ConfigDeserializeErrorKind::CargoMetadataError(
158                    CargoMetadataError::InvalidToml(err),
159                ),
160            }
161            .into());
162        }
163    };
164
165    match v {
166        toml::Value::Table(mut v) => {
167            let pkg = v
168                .remove("package")
169                .and_then(|v| remove_toml_key_path(v, ["metadata", app_name]));
170            let wsp = v
171                .remove("workspace")
172                .and_then(|v| remove_toml_key_path(v, ["metadata", app_name]));
173
174            let v = if let Some(pkg) = pkg {
175                if let Some(_) = wsp {
176                    return Err(ConfigDeserializeError {
177                        filename: filename.to_string(),
178                        format,
179                        kind: ConfigDeserializeErrorKind::CargoMetadataError(
180                            CargoMetadataError::FoundMultiple,
181                        ),
182                    }
183                    .into());
184                } else {
185                    pkg
186                }
187            } else {
188                if let Some(v) = wsp {
189                    v
190                } else {
191                    return Ok(None);
192                }
193            };
194
195            let data: T = v.try_into().or_else(|err| {
196                Err(ConfigDeserializeError {
197                    filename: filename.to_string(),
198                    format,
199                    kind: ConfigDeserializeErrorKind::CargoMetadataError(
200                        CargoMetadataError::InvalidDataStructure(err),
201                    ),
202                })
203            })?;
204
205            return Ok(Some(ConfigFileData {
206                filename: "Cargo.toml".to_string(),
207                format: ConfigFormat::CargoMetadata,
208                data,
209            }));
210        }
211        _ => {
212            return Err(ConfigDeserializeError {
213                filename: filename.to_string(),
214                format,
215                kind: ConfigDeserializeErrorKind::CargoMetadataError(
216                    CargoMetadataError::CargoTomlIsNotTable,
217                ),
218            }
219            .into())
220        }
221    }
222}
223
224pub fn find_config_file<T: DeserializeOwned>(
225    file_or_dir_path: Option<&str>,
226    app_name: &str,
227) -> Result<ConfigFileData<T>, FindConfigError> {
228    let file_or_dir_path = file_or_dir_path.unwrap_or("");
229
230    let path = Path::new(file_or_dir_path);
231
232    if file_or_dir_path.is_empty() || file_or_dir_path == "." || path.is_dir() {
233        find_config_file_in_dir(path, app_name)
234    } else {
235        let filename = path.file_name().unwrap_or_default();
236        if filename == "Cargo.toml" {
237            if let Some(conf) = read_config_from_cargo_toml(path, app_name)? {
238                Ok(conf)
239            } else {
240                // Cargo.toml exists but lack config
241                Err(FindConfigError::ReadError(
242                    ReadConfigError::DeserializeError(ConfigDeserializeError {
243                        filename: path.to_string_lossy().into_owned(),
244                        format: ConfigFormat::CargoMetadata,
245                        kind: ConfigDeserializeErrorKind::CargoMetadataError(
246                            CargoMetadataError::NoData,
247                        ),
248                    }),
249                ))
250            }
251        } else {
252            let filename = filename.to_string_lossy();
253            for (format, ext) in EXTENSIONS {
254                if filename.ends_with(ext) {
255                    let conf = read_config_from_file_and_format(path, format)?;
256
257                    return Ok(conf);
258                }
259            }
260
261            Err(FindConfigError::UnknownExtension {
262                extension: path
263                    .extension()
264                    .map(|ext| ext.to_string_lossy().into_owned()),
265                file: path.to_string_lossy().into_owned(),
266            })
267        }
268    }
269}
270
271fn remove_toml_key_path<'a>(
272    mut toml: toml::Value,
273    path: impl IntoIterator<Item = &'a str>,
274) -> Option<toml::Value> {
275    for key in path {
276        toml = toml.as_table_mut()?.remove(key)?;
277    }
278
279    Some(toml)
280}