refactor: extract activity dispatcher middleware to eliminate handler boilerplate #10

Open
opened 2026-05-30 01:32:05 +00:00 by GKaszewski · 0 comments
Owner

Problem

All 13 activity handlers in activities/*.rs repeat the same pattern: check_guards() → parse actor/object → call repos → log. The Activity trait interface is nearly as complex as each implementation. Every handler independently calls into data.follow_repo, data.blocklist_repo, etc.

To understand a single flow (e.g. "how does a Follow get accepted") you must jump between follow.rsaccept.rsfollow_repo trait → service/follow.rs. Testing a single activity requires mocking 5+ repositories.

Proposal

Extract a unified activity dispatcher/middleware that:

  1. Handles check_guards() (dedup + domain blocklist) once for all activities
  2. Provides a simplified handler trait where implementations only define type-specific receive() logic
  3. Moves common verification patterns (domain matching, actor resolution) into the middleware

Before

// Every handler repeats this
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
    if check_guards(&self.id, self.actor.inner(), data).await? {
        return Ok(());
    }
    // type-specific logic...
}

After

// Middleware handles guards, handler only has business logic
impl ActivityHandler for FollowActivity {
    async fn handle(self, ctx: &ActivityContext) -> Result<()> {
        // only type-specific logic
    }
}

Files

  • src/activities/*.rs (all 13 handlers)
  • src/activities/helpers.rs
  • src/inbox.rs

Trade-offs

  • Reduces boilerplate and eliminates guard duplication
  • May reduce flexibility if a handler needs non-standard guard behavior
  • Migration is mechanical but touches many files

Dependency

Should be done before #10 (Undo dispatcher) since Undo's match arms would become registered handlers.

## Problem All 13 activity handlers in `activities/*.rs` repeat the same pattern: `check_guards()` → parse actor/object → call repos → log. The `Activity` trait interface is nearly as complex as each implementation. Every handler independently calls into `data.follow_repo`, `data.blocklist_repo`, etc. To understand a single flow (e.g. "how does a Follow get accepted") you must jump between `follow.rs` → `accept.rs` → `follow_repo` trait → `service/follow.rs`. Testing a single activity requires mocking 5+ repositories. ## Proposal Extract a unified activity dispatcher/middleware that: 1. Handles `check_guards()` (dedup + domain blocklist) once for all activities 2. Provides a simplified handler trait where implementations only define type-specific `receive()` logic 3. Moves common verification patterns (domain matching, actor resolution) into the middleware ### Before ```rust // Every handler repeats this async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> { if check_guards(&self.id, self.actor.inner(), data).await? { return Ok(()); } // type-specific logic... } ``` ### After ```rust // Middleware handles guards, handler only has business logic impl ActivityHandler for FollowActivity { async fn handle(self, ctx: &ActivityContext) -> Result<()> { // only type-specific logic } } ``` ## Files - `src/activities/*.rs` (all 13 handlers) - `src/activities/helpers.rs` - `src/inbox.rs` ## Trade-offs - Reduces boilerplate and eliminates guard duplication - May reduce flexibility if a handler needs non-standard guard behavior - Migration is mechanical but touches many files ## Dependency Should be done before #10 (Undo dispatcher) since Undo's match arms would become registered handlers.
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: GKaszewski/k-ap#10