1#![doc = include_str!("../README.md")]
2
3mod config;
4mod output;
5mod ty;
6mod value;
7
8#[cfg(test)]
9mod tests;
10
11use std::path::{Path, PathBuf};
12
13use toml_edit::TomlError;
14
15pub use self::{
16 config::{Config, ConfigItem},
17 output::OutputFormat,
18 ty::ConfigType,
19 value::ConfigValue,
20};
21
22pub enum ConfigErr {
24 Parse(TomlError),
26 InvalidValue,
28 InvalidType,
30 ValueTypeMismatch,
32 Other(String),
34}
35
36impl From<TomlError> for ConfigErr {
37 fn from(e: TomlError) -> Self {
38 Self::Parse(e)
39 }
40}
41
42impl core::fmt::Display for ConfigErr {
43 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
44 match self {
45 Self::Parse(e) => write!(f, "{}", e),
46 Self::InvalidValue => write!(f, "Invalid config value"),
47 Self::InvalidType => write!(f, "Invalid config type"),
48 Self::ValueTypeMismatch => write!(f, "Config value and type mismatch"),
49 Self::Other(s) => write!(f, "{}", s),
50 }
51 }
52}
53
54impl core::fmt::Debug for ConfigErr {
55 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
56 write!(f, "{}", self)
57 }
58}
59
60impl std::error::Error for ConfigErr {}
61
62pub type ConfigResult<T> = Result<T, ConfigErr>;
64
65#[derive(Debug, Clone)]
67pub struct GenerateOptions {
68 pub specs: Vec<PathBuf>,
70 pub oldconfig: Option<PathBuf>,
72 pub output: Option<PathBuf>,
74 pub fmt: OutputFormat,
76 pub writes: Vec<String>,
78 pub keep_backup: bool,
80}
81
82impl GenerateOptions {
83 pub fn new(specs: impl IntoIterator<Item = PathBuf>) -> Self {
85 Self {
86 specs: specs.into_iter().collect(),
87 oldconfig: None,
88 output: None,
89 fmt: OutputFormat::Toml,
90 writes: Vec::new(),
91 keep_backup: false,
92 }
93 }
94}
95
96#[derive(Debug, Clone)]
98pub struct GenerateReport {
99 pub untouched: Vec<ConfigItem>,
101 pub extra: Vec<ConfigItem>,
103 pub output: String,
105}
106
107#[derive(Debug)]
109pub struct LoadReport {
110 pub config: Config,
112 pub untouched: Vec<ConfigItem>,
114 pub extra: Vec<ConfigItem>,
116}
117
118pub fn parse_config_read_arg(arg: &str) -> ConfigResult<(String, String)> {
120 if let Some((table, key)) = arg.split_once('.') {
121 Ok((table.into(), key.into()))
122 } else {
123 Ok((Config::GLOBAL_TABLE_NAME.into(), arg.into()))
124 }
125}
126
127pub fn parse_config_write_arg(arg: &str) -> ConfigResult<(String, String, String)> {
129 let (item, value) = arg.split_once('=').ok_or_else(|| {
130 ConfigErr::Other(format!(
131 "Invalid config setting command `{}`, expected `table.key=value`",
132 arg
133 ))
134 })?;
135 if let Some((table, key)) = item.split_once('.') {
136 Ok((table.into(), key.into(), value.into()))
137 } else {
138 Ok((Config::GLOBAL_TABLE_NAME.into(), item.into(), value.into()))
139 }
140}
141
142pub fn load_config_specs(specs: &[PathBuf]) -> ConfigResult<Config> {
144 let mut config = Config::new();
145 for spec in specs {
146 let spec_toml = std::fs::read_to_string(spec).map_err(|err| {
147 ConfigErr::Other(format!(
148 "Failed to read config specification file {}: {}",
149 spec.display(),
150 err
151 ))
152 })?;
153 let sub_config = Config::from_toml(&spec_toml)?;
154 config.merge(&sub_config)?;
155 }
156 Ok(config)
157}
158
159pub fn load_config(path: impl AsRef<Path>) -> ConfigResult<Config> {
161 let path = path.as_ref();
162 let toml = std::fs::read_to_string(path).map_err(|err| {
163 ConfigErr::Other(format!(
164 "Failed to read config file {}: {}",
165 path.display(),
166 err
167 ))
168 })?;
169 Config::from_toml(&toml)
170}
171
172pub fn apply_config_writes(config: &mut Config, writes: &[String]) -> ConfigResult<()> {
174 for arg in writes {
175 let (table, key, value) = parse_config_write_arg(arg)?;
176 let new_value = ConfigValue::new(&value)?;
177 let item = config
178 .config_at_mut(&table, &key)
179 .ok_or_else(|| ConfigErr::Other(format!("Config item `{}` not found", arg)))?;
180 item.value_mut().update(new_value)?;
181 }
182 Ok(())
183}
184
185pub fn load_config_state(options: &GenerateOptions) -> ConfigResult<LoadReport> {
187 let mut config = load_config_specs(&options.specs)?;
188 let (untouched, extra) = if let Some(oldconfig_path) = &options.oldconfig {
189 let oldconfig = load_config(oldconfig_path)?;
190 config.update(&oldconfig)?
191 } else {
192 (Vec::new(), Vec::new())
193 };
194
195 apply_config_writes(&mut config, &options.writes)?;
196 Ok(LoadReport {
197 config,
198 untouched,
199 extra,
200 })
201}
202
203pub fn generate_config(options: &GenerateOptions) -> ConfigResult<GenerateReport> {
205 let report = load_config_state(options)?;
206 let LoadReport {
207 config,
208 untouched,
209 extra,
210 } = report;
211 let output = config.dump(options.fmt.clone())?;
212 if let Some(path) = options.output.as_deref() {
213 write_config_output(path, &output, options.keep_backup)?;
214 }
215
216 Ok(GenerateReport {
217 untouched,
218 extra,
219 output,
220 })
221}
222
223pub fn read_config_value(specs: &[PathBuf], item: &str) -> ConfigResult<String> {
225 let config = load_config_specs(specs)?;
226 read_loaded_config_value(&config, item)
227}
228
229pub fn read_config_string(specs: &[PathBuf], item: &str) -> ConfigResult<String> {
231 let config = load_config_specs(specs)?;
232 read_loaded_config_string(&config, item)
233}
234
235pub fn read_loaded_config_value(config: &Config, item: &str) -> ConfigResult<String> {
237 Ok(find_config_item(config, item)?.value().to_toml_value())
238}
239
240pub fn read_loaded_config_string(config: &Config, item: &str) -> ConfigResult<String> {
242 find_config_item(config, item)?
243 .value()
244 .as_str()
245 .map(ToOwned::to_owned)
246 .ok_or_else(|| ConfigErr::Other(format!("Config item `{}` is not a string", item)))
247}
248
249fn find_config_item<'a>(config: &'a Config, item: &str) -> ConfigResult<&'a ConfigItem> {
250 let (table, key) = parse_config_read_arg(item)?;
251 config
252 .config_at(&table, &key)
253 .ok_or_else(|| ConfigErr::Other(format!("Config item `{}` not found", item)))
254}
255
256fn write_config_output(path: &Path, output: &str, keep_backup: bool) -> ConfigResult<()> {
257 if let Some(parent) = path.parent() {
258 std::fs::create_dir_all(parent).map_err(|err| {
259 ConfigErr::Other(format!(
260 "Failed to create output directory {}: {}",
261 parent.display(),
262 err
263 ))
264 })?;
265 }
266 if let Ok(oldconfig) = std::fs::read_to_string(path) {
267 if oldconfig == output {
268 return Ok(());
269 }
270 if keep_backup {
271 let bak_path = if let Some(ext) = path.extension() {
272 path.with_extension(format!("old.{}", ext.to_string_lossy()))
273 } else {
274 path.with_extension("old")
275 };
276 std::fs::write(&bak_path, oldconfig).map_err(|err| {
277 ConfigErr::Other(format!(
278 "Failed to write backup config file {}: {}",
279 bak_path.display(),
280 err
281 ))
282 })?;
283 }
284 }
285 std::fs::write(path, output).map_err(|err| {
286 ConfigErr::Other(format!(
287 "Failed to write config file {}: {}",
288 path.display(),
289 err
290 ))
291 })
292}