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 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 }
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 }
82 Err(err) => {
83 match err {
84 ReadConfigError::OpenFileError { error, .. }
85 if error.kind() == io::ErrorKind::NotFound =>
86 {
87 }
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 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}