Commit
+85 -63 +/-8 browse
1 | diff --git a/Cargo.lock b/Cargo.lock |
2 | index eeeab47..0dc2cac 100644 |
3 | --- a/Cargo.lock |
4 | +++ b/Cargo.lock |
5 | @@ -583,6 +583,7 @@ dependencies = [ |
6 | "log", |
7 | "serde", |
8 | "serde_json", |
9 | + "tempfile", |
10 | "thiserror", |
11 | "toml 0.7.8", |
12 | "toml_edit 0.22.14", |
13 | diff --git a/ayllu-mail/src/config.rs b/ayllu-mail/src/config.rs |
14 | index 3447513..dc05a68 100644 |
15 | --- a/ayllu-mail/src/config.rs |
16 | +++ b/ayllu-mail/src/config.rs |
17 | @@ -1,9 +1,12 @@ |
18 | + use std::path::PathBuf; |
19 | + |
20 | use serde::{Deserialize, Serialize}; |
21 | - use std::path::{Path, PathBuf}; |
22 | |
23 | - use ayllu_config::{data_dir, runtime_dir, Configurable, Error, Reader}; |
24 | + use ayllu_config::{data_dir, runtime_dir, Configurable}; |
25 | use mailpot::{Configuration as MailPotConfig, SendMail}; |
26 | |
27 | + pub const EXAMPLE_CONFIG: &str = include_str!("../../config.example.toml"); |
28 | + |
29 | #[derive(Serialize, Deserialize, Clone)] |
30 | pub struct Database { |
31 | pub migrate: Option<bool>, |
32 | @@ -203,7 +206,3 @@ impl Config { |
33 | } |
34 | |
35 | impl Configurable for Config {} |
36 | - |
37 | - pub fn load(path: Option<&Path>) -> Result<Config, Error> { |
38 | - Reader::load(path) |
39 | - } |
40 | diff --git a/ayllu-mail/src/main.rs b/ayllu-mail/src/main.rs |
41 | index 64936dd..ef7c926 100644 |
42 | --- a/ayllu-mail/src/main.rs |
43 | +++ b/ayllu-mail/src/main.rs |
44 | @@ -52,13 +52,16 @@ enum Postfix { |
45 | Maps {}, |
46 | } |
47 | |
48 | - #[derive(Subcommand, Debug, PartialEq)] |
49 | + #[derive(Subcommand, Debug)] |
50 | enum Commands { |
51 | /// generate autocomplete commands for common shells |
52 | Complete { |
53 | #[arg(long)] |
54 | shell: Shell, |
55 | }, |
56 | + /// Configuration options. |
57 | + #[clap(subcommand)] |
58 | + Config(ayllu_config::Command), |
59 | /// run the rpc server and mailing list manager |
60 | Serve {}, |
61 | /// post a new message into the mail queue from stdin |
62 | @@ -89,32 +92,32 @@ fn init_logger(level: Level) { |
63 | #[allow(clippy::type_complexity)] |
64 | fn main() -> Result<(), Box<dyn std::error::Error>> { |
65 | let cli = Cli::parse(); |
66 | - let mut ayllu_config = config::load(cli.config.as_deref())?; |
67 | + let mut mail_cfg: config::Config = ayllu_config::Reader::load(cli.config.as_deref())?; |
68 | if let Some(db_path) = cli.database { |
69 | - ayllu_config.mail.database.path.clone_from(&db_path); |
70 | + mail_cfg.mail.database.path.clone_from(&db_path); |
71 | } |
72 | // ensure the database path exists since mailpot doesn't create it for us |
73 | std::fs::create_dir_all( |
74 | - ayllu_config |
75 | + mail_cfg |
76 | |
77 | .database |
78 | .path |
79 | .parent() |
80 | .expect("db path has no parent"), |
81 | )?; |
82 | - declarative::initialize(&ayllu_config)?; |
83 | + declarative::initialize(&mail_cfg)?; |
84 | match cli.command { |
85 | Commands::Complete { shell } => { |
86 | let mut cmd = Cli::command(); |
87 | print_completions(shell, &mut cmd); |
88 | } |
89 | + Commands::Config(command) => { |
90 | + command.execute::<config::Config>(config::EXAMPLE_CONFIG, cli.config)?; |
91 | + } |
92 | Commands::Serve {} => { |
93 | - init_logger( |
94 | - cli.level |
95 | - .unwrap_or(Level::from_str(&ayllu_config.log_level)?), |
96 | - ); |
97 | - let cfg = ayllu_config.clone(); |
98 | - let http_cfg = ayllu_config.clone(); |
99 | + init_logger(cli.level.unwrap_or(Level::from_str(&mail_cfg.log_level)?)); |
100 | + let cfg = mail_cfg.clone(); |
101 | + let http_cfg = cfg.clone(); |
102 | std::thread::spawn(move || { |
103 | TokioBuilder::new_current_thread() |
104 | .enable_all() |
105 | @@ -129,24 +132,18 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { |
106 | .block_on(async move { LocalSet::new().run_until(server::serve(&cfg)).await })?; |
107 | } |
108 | Commands::Post {} => { |
109 | - init_logger( |
110 | - cli.level |
111 | - .unwrap_or(Level::from_str(&ayllu_config.log_level)?), |
112 | - ); |
113 | + init_logger(cli.level.unwrap_or(Level::from_str(&mail_cfg.log_level)?)); |
114 | let mut input = String::new(); |
115 | stdin().read_to_string(&mut input)?; |
116 | let envelope = Envelope::from_bytes(input.as_bytes(), None)?; |
117 | - let mut db = Connection::open_or_create_db(ayllu_config.mailpot_config())?.trusted(); |
118 | + let mut db = Connection::open_or_create_db(mail_cfg.mailpot_config())?.trusted(); |
119 | let tx = db.transaction(TransactionBehavior::Exclusive)?; |
120 | tx.post(&envelope, input.as_bytes(), false)?; |
121 | tx.commit()?; |
122 | } |
123 | Commands::Send {} => { |
124 | - init_logger( |
125 | - cli.level |
126 | - .unwrap_or(Level::from_str(&ayllu_config.log_level)?), |
127 | - ); |
128 | - let mut db = Connection::open_or_create_db(ayllu_config.mailpot_config())?.trusted(); |
129 | + init_logger(cli.level.unwrap_or(Level::from_str(&mail_cfg.log_level)?)); |
130 | + let mut db = Connection::open_or_create_db(mail_cfg.mailpot_config())?.trusted(); |
131 | let tx = db.transaction(TransactionBehavior::Exclusive)?; |
132 | let messages = tx.queue(Queue::Out)?; |
133 | let mut failed: Vec<QueueEntry> = Vec::with_capacity(messages.len()); |
134 | @@ -192,7 +189,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { |
135 | Ok(()) |
136 | } |
137 | for message in messages { |
138 | - if let Err(err) = submit(&ayllu_config.mail.sendmail_command, &message) { |
139 | + if let Err(err) = submit(&mail_cfg.mail.sendmail_command, &message) { |
140 | log::error!( |
141 | "error sending message: {}\n{}", |
142 | message.message_id, |
143 | @@ -210,7 +207,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { |
144 | Commands::Postfix { command } => { |
145 | let this_exe = std::env::current_exe().unwrap(); |
146 | let cfg = PostfixConfiguration { |
147 | - user: std::borrow::Cow::from(ayllu_config.mail.postfix_user.clone()), |
148 | + user: std::borrow::Cow::from(mail_cfg.mail.postfix_user.clone()), |
149 | binary_path: this_exe, |
150 | ..Default::default() |
151 | }; |
152 | @@ -219,7 +216,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { |
153 | print!( |
154 | "{}", |
155 | cfg.generate_master_cf_entry( |
156 | - &ayllu_config.mailpot_config(), |
157 | + &mail_cfg.mailpot_config(), |
158 | cli.config |
159 | .expect("need to specify an absolute path to your config file") |
160 | .as_path() |
161 | @@ -228,7 +225,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { |
162 | } |
163 | Postfix::Maps {} => { |
164 | let mut db = |
165 | - Connection::open_or_create_db(ayllu_config.mailpot_config())?.trusted(); |
166 | + Connection::open_or_create_db(mail_cfg.mailpot_config())?.trusted(); |
167 | let tx = db.transaction(TransactionBehavior::Exclusive)?; |
168 | let lists: Result< |
169 | Vec<(DbVal<MailingList>, Option<DbVal<PostPolicy>>)>, |
170 | diff --git a/ayllu/src/config.rs b/ayllu/src/config.rs |
171 | index 590ef41..e3f2cd5 100644 |
172 | --- a/ayllu/src/config.rs |
173 | +++ b/ayllu/src/config.rs |
174 | @@ -134,7 +134,7 @@ pub struct TreeSitter { |
175 | pub parsers: Option<Vec<TreeSitterParser>>, |
176 | #[serde(default = "TreeSitter::default_keywords")] |
177 | pub keywords: Vec<String>, |
178 | - pub highlights: Option<HashMap<String, String>> |
179 | + pub highlights: Option<HashMap<String, String>>, |
180 | } |
181 | |
182 | impl TreeSitter { |
183 | @@ -349,10 +349,6 @@ Disallow: /*/*/chart/* |
184 | fn default_max_blocking_threads() -> NonZeroUsize { |
185 | NonZeroUsize::new(512).unwrap() |
186 | } |
187 | - |
188 | - pub fn to_json(&self) -> String { |
189 | - serde_json::to_string(self).unwrap() |
190 | - } |
191 | } |
192 | |
193 | #[cfg(test)] |
194 | diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml |
195 | index c042d5b..64c1727 100644 |
196 | --- a/crates/config/Cargo.toml |
197 | +++ b/crates/config/Cargo.toml |
198 | @@ -13,3 +13,6 @@ toml_edit = { version = "0.22.14", features = ["serde"] } |
199 | thiserror = "1.0.61" |
200 | clap = "4.5.9" |
201 | serde_json = "1.0.120" |
202 | + |
203 | + [dev-dependencies] |
204 | + tempfile = "3.10.1" |
205 | diff --git a/crates/config/src/edit.rs b/crates/config/src/edit.rs |
206 | index 4a104df..ced3bce 100644 |
207 | --- a/crates/config/src/edit.rs |
208 | +++ b/crates/config/src/edit.rs |
209 | @@ -29,30 +29,19 @@ struct Visitor { |
210 | |
211 | impl VisitMut for Visitor { |
212 | fn visit_table_like_kv_mut(&mut self, key: toml_edit::KeyMut<'_>, node: &mut Item) { |
213 | - if self.found.is_some() { |
214 | - return; |
215 | - } |
216 | - match node { |
217 | - Item::None => {} |
218 | - Item::Value(value) => { |
219 | - let mut copy = self.state.clone(); |
220 | - copy.append(&mut vec![key.to_string()]); |
221 | - if copy.join(".") == self.target { |
222 | - if let Some(new_value) = &self.replace { |
223 | - *value = v2v(new_value.clone()); |
224 | - } |
225 | - self.found = Some(value.clone()); |
226 | - } |
227 | - self.state.clear(); |
228 | - } |
229 | - Item::Table(_) => { |
230 | - self.state.push(key.to_string()); |
231 | - toml_edit::visit_mut::visit_table_like_kv_mut(self, key, node); |
232 | - } |
233 | - Item::ArrayOfTables(_) => { |
234 | - // arrays are not supported |
235 | - } |
236 | + self.state.push(key.to_string()); |
237 | + toml_edit::visit_mut::visit_table_like_kv_mut(self, key, node); |
238 | + self.state.pop(); |
239 | + } |
240 | + |
241 | + fn visit_value_mut(&mut self, node: &mut Value) { |
242 | + if self.state.join(".") == self.target { |
243 | + if let Some(replacement) = &self.replace { |
244 | + *node = v2v(replacement.clone()); |
245 | + }; |
246 | + self.found = Some(node.clone()); |
247 | } |
248 | + toml_edit::visit_mut::visit_value_mut(self, node); |
249 | } |
250 | } |
251 | |
252 | @@ -81,9 +70,7 @@ impl Editor { |
253 | let mut doc = cfg_str.parse::<DocumentMut>()?; |
254 | let mut writer = Visitor { |
255 | target: key.to_string(), |
256 | - replace: None, |
257 | - state: vec![], |
258 | - found: None, |
259 | + ..Default::default() |
260 | }; |
261 | writer.visit_document_mut(&mut doc); |
262 | if let Some(value) = writer.found { |
263 | @@ -121,3 +108,42 @@ impl Editor { |
264 | Ok(()) |
265 | } |
266 | } |
267 | + |
268 | + #[cfg(test)] |
269 | + mod tests { |
270 | + |
271 | + use std::io::Write; |
272 | + use tempfile::NamedTempFile; |
273 | + |
274 | + use super::*; |
275 | + |
276 | + #[test] |
277 | + fn test_get_key() { |
278 | + let mut fp = NamedTempFile::new().unwrap(); |
279 | + write!( |
280 | + fp, |
281 | + r#" |
282 | + hello = "world" |
283 | + |
284 | + [fuu] |
285 | + bar = "baz" |
286 | + qux = 1 |
287 | + |
288 | + [a] |
289 | + |
290 | + [a.b] |
291 | + c = false |
292 | + |
293 | + [a.d] |
294 | + nested = {{e = "f"}} |
295 | + "# |
296 | + ) |
297 | + .unwrap(); |
298 | + let editor = Editor(Some(fp.path().to_path_buf())); |
299 | + assert!(editor.get("hello").unwrap() == " \"world\""); |
300 | + assert!(editor.get("fuu.bar").unwrap() == " \"baz\""); |
301 | + assert!(editor.get("fuu.qux").unwrap() == " 1"); |
302 | + assert!(editor.get("a.b.c").unwrap() == " false"); |
303 | + assert!(editor.get("a.d.nested.e").unwrap() == " \"f\"") |
304 | + } |
305 | + } |
306 | diff --git a/crates/config/src/flags.rs b/crates/config/src/flags.rs |
307 | index 8d74d74..d2f32b8 100644 |
308 | --- a/crates/config/src/flags.rs |
309 | +++ b/crates/config/src/flags.rs |
310 | @@ -34,6 +34,7 @@ pub enum Command { |
311 | } |
312 | |
313 | impl Command { |
314 | + /// execute the config subcommand |
315 | pub fn execute<T>(self, example_cfg: &str, path: Option<PathBuf>) -> Result<(), Error> |
316 | where |
317 | T: DeserializeOwned + Configurable + Serialize, |
318 | diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs |
319 | index 03d898a..2327afc 100644 |
320 | --- a/crates/config/src/lib.rs |
321 | +++ b/crates/config/src/lib.rs |
322 | @@ -1,4 +1,3 @@ |
323 | - pub use edit::Editor; |
324 | pub use error::Error; |
325 | pub use flags::Command; |
326 | pub use reader::{data_dir, runtime_dir, Configurable, Reader}; |