Hi,
I have designed a conveient generic enum that I want to deserialize from a config file. I am still learning Rust and Serde so this is both a help and code review request.
My program is a process scheduler that watches processes based on some given criteria (name, running/not_running, resource usage ...) and executes some actions when criteria matches.
sysinfo::Process can be matched by (name, cmdline or exe_path).
I am using serde to deserialize straight into the appropriate matcher for a process matching = ...
Here is an example toml config file:
[[profiles]]
# this matches PatternIn<String>
matching = { cmdline = "foo_seen" }
[[profiles.commands]]
condition = {seen = "5s"}
exec = ["echo", "seen"]
exec_end = ["echo", "end"]
[[profiles]]
# I want to implement the following
matching = { cmdline = "foo_seen", regex = true}
I designed the following types to be able to match on String, Regex or other future types.
pub trait MatchBy<Criteria>
where
Criteria: Display,
{
fn match_by(&self, matcher: Criteria) -> bool;
}
#[derive(Deserialize, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum PatternIn<P> {
ExePath(P),
Cmdline(P),
Name(P),
}
// This is the main matcher enum that will hold all types of matchers. It is used in the Config type with Deserialize derive trait.
#[derive(Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum ProcessMatcher {
StringPattern(PatternIn<String>),
//RegexPattern(PatternIn<Regex>) // needing help with this one
//CPUUsage
//RamUsage
//Network
}
// the struct where matching is serde parsed into
#[derive(Debug, Deserialize, Clone)]
pub struct Profile {
/// pattern of process name to match against
// pub matching: ProfileMatching,
pub matching: ProcessMatcher,
...
}
trait MatchProcByPattern<P> {
fn matches_exe(&self, pattern: P) -> bool;
fn matches_cmdline(&self, pattern: P) -> bool;
fn matches_name(&self, pattern: P) -> bool;
}
impl<P> MatchBy<PatternIn<P>> for sysinfo::Process
where
sysinfo::Process: MatchProcByPattern<P>,
P: Display
{
fn match_by(&self, matcher: PatternIn<P>) -> bool {
match matcher {
PatternIn::ExePath(pat) => self.matches_exe(pat),
PatternIn::Cmdline(pat) => self.matches_cmdline(pat),
PatternIn::Name(pat) => self.matches_name(pat),
}
}
}
impl MatchBy<ProcessMatcher> for sysinfo::Process {
fn match_by(&self, matcher: ProcessMatcher) -> bool {
match matcher {
ProcessMatcher::StringPattern(pat) => self.match_by(pat),
}
}
}
I implemented the necessary code to match PatternIn<String>
.
impl MatchProcByPattern<String> for sysinfo::Process {
fn matches_exe(&self, pattern: String) -> bool {
let finder = memmem::Finder::new(&pattern);
self.exe()
.and_then(|exe_name| finder.find(exe_name.as_os_str().as_bytes()))
.is_some()
}
fn matches_cmdline(&self, pattern: String) -> bool {
let finder = memmem::Finder::new(&pattern);
finder.find(self.cmd().join(" ").as_bytes()).is_some()
}
fn matches_name(&self, pattern: String) -> bool {
self.name().contains(&pattern)
}
}
impl MatchProcByPattern<Regex> for sysinfo::Process {
...
}
For the regex part however, the config parsing will be decoupled from the enum variant. I want the user to use the regex = true
bool alongside the PatterIn
variant which will result after deserialization to a PatternIn<Regex>
. My solution has been the following:
#[derive(Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum ProcessMatcher {
StringPattern(PatternIn<String>),
#[serde(deserialize_with = "deserialize_regex_pattern")]
RegexPattern(PatternIn<Regex>) // needing help with this one
}
fn deserialize_regex_pattern<'de, D>(deserializer: D) -> Result<PatternIn<Regex>, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize, Debug)]
struct Pattern {
#[serde(flatten)]
pattern: PatternIn<String>,
regex: bool
}
let pd: Pattern = Pattern::deserialize(deserializer)?;
if !pd.regex {
Err(de::Error::custom("not a regex pattern"))
} else {
match pd.pattern {
PatternIn::Cmdline(pat) => {
match pat.parse::<Regex>() {
Ok(regex) => Ok(PatternIn::Cmdline(regex)),
Err(err) => Err(de::Error::custom(err))
}
},
PatternIn::Name(pat) => {
match pat.parse::<Regex>() {
Ok(regex) => Ok(PatternIn::Name(regex)),
Err(err) => Err(de::Error::custom(err))
}
},
...
}
}
}
As you can see the deserialize_regex_pattern
function needs to have a match for every kind of PatternIn resulting in a lot of code repeat.
Questions:
- Is there a way to avoid code duplication ?
- Is there an other way to achieve this without having to define an extra enum to handle the regex boolean ?