Skip to content

refactor(common): add macro_rules! helpers for SeaORM entity relation boilerplate #769

@geoffjay

Description

@geoffjay

Overview

Every SeaORM entity file in the workspace contains two identical structural boilerplate blocks: an impl Related<T> for Entity that always calls Relation::X.def(), and an empty impl ActiveModelBehavior for ActiveModel {}. Across 13 entity files these are copy-pasted verbatim with only the type names changing. A pair of macro_rules! helpers in agentd-common would collapse each into a single expressive line.

Current State

All 13 entity files follow this closing pattern (example from crates/communicate/src/entity/message.rs:53-59 and crates/orchestrator/src/entity/dispatch.rs:32-39):

// crates/communicate/src/entity/message.rs:53-59
impl Related<super::room::Entity> for Entity {
    fn to() -> RelationDef {
        Relation::Room.def()
    }
}

impl ActiveModelBehavior for ActiveModel {}
// crates/orchestrator/src/entity/dispatch.rs:32-39  (identical shape)
impl Related<super::workflow::Entity> for Entity {
    fn to() -> RelationDef {
        Relation::Workflow.def()
    }
}

impl ActiveModelBehavior for ActiveModel {}

All 13 entity files across communicate, orchestrator, notify, and memory repeat this exact structure.

Desired State

Two macro_rules! macros exported from agentd-common::sea_orm_helpers (behind a sea-orm feature flag) reduce each entity file's tail to:

// After — crates/communicate/src/entity/message.rs
sea_orm_related!(super::room::Entity, Relation::Room);
sea_orm_active_model_behavior!();

Macro definitions in crates/common/src/macros.rs:

/// Generates `impl Related<$target> for Entity` delegating to `$relation.def()`.
#[macro_export]
macro_rules! sea_orm_related {
    ($target:ty, $relation:expr) => {
        impl sea_orm::entity::Related<$target> for Entity {
            fn to() -> sea_orm::entity::RelationDef {
                $relation.def()
            }
        }
    };
}

/// Generates the empty `impl ActiveModelBehavior for ActiveModel {}`.
#[macro_export]
macro_rules! sea_orm_active_model_behavior {
    () => {
        impl sea_orm::entity::ActiveModelBehavior for ActiveModel {}
    };
}

Affected Files

  • crates/common/src/macros.rs — new file with the two macro_rules! definitions
  • crates/common/src/lib.rs — add pub mod macros; (or inline with #[macro_export])
  • crates/common/Cargo.toml — add optional sea-orm dependency gated on a sea-orm feature flag
  • All 13 entity files across communicate, orchestrator, notify, memory — replace boilerplate with macro calls

Entity files:

  • crates/communicate/src/entity/message.rs
  • crates/communicate/src/entity/room.rs
  • crates/communicate/src/entity/participant.rs
  • crates/orchestrator/src/entity/dispatch.rs
  • crates/orchestrator/src/entity/workflow.rs
  • crates/notify/src/entity/notification.rs
  • crates/memory/src/entity/memory_entry.rs
  • (and remaining entity files in those crates)

Acceptance Criteria

  • sea_orm_related! and sea_orm_active_model_behavior! macros defined and #[macro_export]-ed from agentd-common
  • Macros are gated behind a sea-orm Cargo feature in agentd-common to avoid pulling sea-orm into crates that do not use it
  • All 13 entity files updated to use the macros
  • cargo build succeeds for all affected crates
  • cargo test passes with no regressions
  • cargo clippy produces no new warnings
  • No change to generated SeaORM behavior

Notes

  • This uses macro_rules! (declarative macros), not proc-macros — no new crate needed.
  • If a crates/macros proc-macro crate is created as part of refactor(macros): create agentd-macros proc-macro crate with ApiIntoResponse derive #768, consider whether these declarative macros belong there or remain in agentd-common. Declarative macros are simpler and have no compile overhead, so agentd-common is the better home.
  • Entities with multiple relations (belongs-to + has-many) may need multiple sea_orm_related! calls — that is fine.
  • The impl ActiveModelBehavior for ActiveModel {} is always empty in this codebase; if a future entity needs custom behavior, the macro call is simply replaced with a manual impl.

Metadata

Metadata

Assignees

No one assigned

    Labels

    refactorCode improvement without changing behavior

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions