Commit
Author: Kevin Schoon [me@kevinschoon.com]
Hash: 3ec8bcd523f406b59952ded918c7e4baa61bd04f
Timestamp: Mon, 22 Jul 2024 21:32:48 +0000 (4 months ago)

+85 -63 +/-8 browse
Generalize config system, add basic editor
Generalize config system, add basic editor

This generalizes all of the config commands across Ayllu binaries into a
single set of flags for ease of use. Additionally it allows setting arbitrary
values via command line which is useful for initializing containers.
1diff --git a/Cargo.lock b/Cargo.lock
2index 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
14index 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
41index 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 .mail
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
171index 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
195index 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
206index 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
307index 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
319index 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};