Skip to content

Commit

Permalink
Use a better Rule registration technique
Browse files Browse the repository at this point in the history
My first attempt was to use inventory to register the impl Rule trait
objects themselves but that failed because:

1. Rules need to be mutable
2. inventory requires the registered object be const, and Box::new()
   isn't a const fn
3. inventory requires the registered object by Sized

So rather than registering the rules, register a factory you can use to
get a new Box<dyn Rule>.
  • Loading branch information
Notgnoshi committed Oct 20, 2024
1 parent 58fabc0 commit 5e7b73b
Showing 8 changed files with 62 additions and 10 deletions.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ color-eyre = "0.6.2"
directories = "5.0.1"
eyre = "0.6.12"
git2 = "0.19"
inventory = "0.3.15"
serde = { version = "1.0.204", features =["derive"] }
toml = "0.8.14"
tracing = "0.1.40"
33 changes: 33 additions & 0 deletions src/achievement/achievement.rs
Original file line number Diff line number Diff line change
@@ -9,6 +9,39 @@ pub struct Achievement {
// TODO: Identify the repository somehow
}

// TODO: Eventually I'll want to pass the &Config to the factory
type FactoryFunc = fn() -> Box<dyn Rule>;

/// A factory to build [Rule]s
///
/// Each [Rule] needs to provide a [RuleFactory] through [inventory::submit!] to register
/// themselves.
pub struct RuleFactory {
factory: FactoryFunc,
}
// See also: rules/mod.rs:builtin_rules(), and each of the inventory::submit!(...) in each Rule impl
inventory::collect!(RuleFactory);

// sugar
impl RuleFactory {
/// Provide your own factory to build your [Rule]
pub const fn new(factory: FactoryFunc) -> Self {
Self { factory }
}

/// Create a [RuleFactory] that uses [Default] to build your [Rule]
pub const fn default<R: Rule + Default + 'static>() -> Self {
RuleFactory {
factory: || Box::new(R::default()) as Box<dyn Rule>,
}
}

/// Use the factory to build the [Rule]
pub fn build(&self) -> Box<dyn Rule> {
(self.factory)()
}
}

/// Defines a [Rule] to grant [Achievement]s
// TODO: How could user-contrib rule _scripts_ work? Consume commits via stdin, emit achievement
// JSON on stdout?
2 changes: 1 addition & 1 deletion src/achievement/mod.rs
Original file line number Diff line number Diff line change
@@ -5,5 +5,5 @@ mod process_rules;
#[cfg(test)]
mod test_process_rules;

pub use achievement::{Achievement, LoggedRule, Rule};
pub use achievement::{Achievement, LoggedRule, Rule, RuleFactory};
pub use process_rules::{grant, grant_with_rules};
1 change: 1 addition & 0 deletions src/achievement/process_rules.rs
Original file line number Diff line number Diff line change
@@ -147,6 +147,7 @@ pub fn grant<'repo>(
reference: &str,
repo: &'repo git2::Repository,
) -> eyre::Result<impl Iterator<Item = Achievement> + 'repo> {
// TODO: Eventually I'll want to use &Config to determine what rules to use
grant_with_rules(reference, repo, crate::rules::builtin_rules())
}

5 changes: 4 additions & 1 deletion src/rules/h001_fixup.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
use crate::achievement::{Achievement, Rule};
use crate::achievement::{Achievement, Rule, RuleFactory};

/// Grant achievements for commits starting with
///
/// * `fixup!`, `squash!`, `amend!` (generated by `git commit --fixup|--squash`)
/// * `WIP:`, `TODO:`, `FIXME:` `DROPME:` (ad-hoc patterns that I've seen in the wild)
#[derive(Default)]
pub struct Fixup;

inventory::submit!(RuleFactory::default::<Fixup>());

const FIXUP_PREFIXES: [&str; 11] = [
"fixup!", "squash!", "amend!", "WIP", "TODO", "FIXME", "DROPME",
// avoid false positives by accepting false negatives. Of all these patterns, "wip" is the one
9 changes: 8 additions & 1 deletion src/rules/h002_shortest_subject_line.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
use crate::achievement::{Achievement, Rule};
use crate::achievement::{Achievement, Rule, RuleFactory};

/// The shortest subject line in a branch
#[derive(Default)]
pub struct ShortestSubjectLine {
shortest_so_far: Option<(git2::Oid, usize)>,
}

// As a proof of concept, don't use Default
fn my_factory() -> Box<dyn Rule> {
Box::new(ShortestSubjectLine::default())
}
inventory::submit!(RuleFactory::new(my_factory));
// inventory::submit!(RuleFactory::default::<ShortestSubjectLine>());

fn subject_length(commit: &git2::Commit) -> usize {
match commit.summary() {
Some(subject) => subject.len(),
14 changes: 7 additions & 7 deletions src/rules/mod.rs
Original file line number Diff line number Diff line change
@@ -2,13 +2,13 @@
mod h001_fixup;
mod h002_shortest_subject_line;

use crate::achievement::Rule;
use crate::achievement::{Rule, RuleFactory};

/// Get a new instance of each builtin [Rule]
pub fn builtin_rules() -> Vec<Box<dyn Rule>> {
// TODO: Rule factory? How to get Rules to register themselves? Through a static singleton +
// module ctor? Metaprogramming? Proc Macro?
vec![
Box::new(h001_fixup::Fixup) as Box<dyn Rule>,
Box::new(h002_shortest_subject_line::ShortestSubjectLine::default()) as Box<dyn Rule>,
]
// Each Rule uses inventory::submit! to register a factory to build themselves with.
inventory::iter::<RuleFactory>
.into_iter()
.map(|factory| factory.build())
.collect()
}

0 comments on commit 5e7b73b

Please sign in to comment.