By far the best solution with the minimum maintenance required and what I finally settled with is as follows:
|- build.rs
|- cargo.toml
|- src
| |- main.rs
| |- handler.rs
| |- handlers.rs //generated
| |- handlers
| | |- handler1.rs
| | |- handler2.rs
handler.rs
contains the Handler
trait:
trait Handler {
fn handle(message: str) -> bool;
}
handlers.rs
is generated at build time via build.rs
:
mod handler1; mod handler2; //...
use crate::handler::*;
use handler1::Handler1; use handler2::Handler2; //...
pub fn get_handlers() -> Vec<Box<dyn Handler>> {
return vec![Box::new(Handler1), Box::new(Handler2)];
}
cargo.toml
must have the build entry supplied:
[package]
build = "build.rs"
edition = "2021"
name = "distributed_handlers"
#...
and finally build.rs
looks like this:
use std::{env, fs};
macro_rules! print {
($($tokens: tt)*) => {
println!("cargo:warning={}", format!($($tokens)*))
}
}
const TEMPLATE: &str = r#"
// Generated bindings to modules
$[mods]
use crate::handler::*;
$[uses]
pub fn get_handlers() -> Vec<Box<dyn Handler>> {
$[inits]
}
"#;
fn main() {
let cur = env::current_dir().unwrap();
let path = String::from(cur.to_string_lossy());
let mut mods = String::from("");
let mut uses = String::from("");
let mut inits: Vec<String> = vec![];
for entry in fs::read_dir(path.clone() + "/src/handlers").unwrap() {
let entry: fs::DirEntry = entry.unwrap();
let name: String = entry.file_name().into_string().unwrap();
let name: String = String::from(name.split_at(name.len() - 3).0);
let mut proper: String = name.clone();
let proper: String = format!("{}{proper}", proper.remove(0).to_uppercase());
mods.push_str(&String::from(format!("mod {};", name)));
uses.push_str(&String::from(format!("use {}::{};", name, proper)));
inits.push(format!("Box::new({})", proper));
}
let inits = format!("return vec![{}];", inits.join(","));
let mut template = String::from(TEMPLATE);
template = template.replace("$[mods]", &mods);
template = template.replace("$[uses]", &uses);
template = template.replace("$[inits]", &inits);
let _ = fs::write(path.clone() + "/src/handlers.rs", template.clone());
for s in template.split("\n") {
print!("{s}");
}
print!("Written to {path}/src/handlers.rs")
}
The above code assumes your module is named the same (just different casing) from the struct / handler itself. A full example is on github.
Edit: I refactored the build script, maybe it's a little more readable now:
let current_dir = env::current_dir().unwrap();
let path = current_dir.to_string_lossy().to_string() + "/src/people";
let (mods, uses, inits) = fs::read_dir(path.clone())
.unwrap()
.map(|entry| {
let entry = entry.unwrap();
let name = entry.file_name().into_string().unwrap();
let name = String::from(name.split_at(name.len() - 3).0);
let proper = name.chars().next().unwrap().to_uppercase().to_string() + &name[1..];
(
format!("mod {name};\n"),
format!("use {name}::{proper};\n"),
format!("Box::new({proper})"),
)
})
.fold(
(String::new(), String::new(), Vec::new()),
|(mut mods, mut uses, mut inits), (m, u, i)| {
mods.push_str(&m);
uses.push_str(&u);
inits.push(i);
(mods, uses, inits)
},
);
let inits = inits.join(",");
let template = TEMPLATE
.replace("$[mods]", &mods)
.replace("$[uses]", &uses)
.replace("$[inits]", &inits);
let _ = fs::write(
current_dir.to_string_lossy().to_string() + "/src/people.rs",
template.clone(),
);