Skip to main content

cja/jobs/
registry.rs

1use crate::app_state::{self};
2
3use super::worker::JobFromDB;
4
5/// A trait for job registries that can dispatch jobs based on their name.
6///
7/// This trait is typically implemented using the `impl_job_registry!` macro,
8/// which generates the necessary dispatch logic for all registered job types.
9#[async_trait::async_trait]
10pub trait JobRegistry<AppState: app_state::AppState> {
11    /// Run a job from the database by dispatching to the appropriate handler.
12    async fn run_job(
13        &self,
14        job: &JobFromDB,
15        app_state: AppState,
16        cancellation_token: tokio_util::sync::CancellationToken,
17    ) -> color_eyre::Result<()>;
18
19    /// The names of every job type registered with this registry.
20    ///
21    /// Used for boot-time manifest emission (see [`crate::eyes_manifest`]).
22    /// The `impl_job_registry!` macro implements this automatically from the
23    /// registered job types' `NAME` constants.
24    fn job_names() -> &'static [&'static str]
25    where
26        Self: Sized;
27}
28
29/// A macro for implementing a job registry that handles job dispatch.
30///
31/// This macro generates a `Jobs` struct that implements `JobRegistry` for your application state.
32/// It creates a match statement that routes jobs to their appropriate handlers based on the job name.
33///
34/// # Usage
35///
36/// ```rust
37/// use cja::impl_job_registry;
38/// use cja::jobs::Job;
39/// use cja::app_state::AppState;
40/// use cja::server::cookies::CookieKey;
41/// use serde::{Serialize, Deserialize};
42///
43/// // Define your app state
44/// #[derive(Clone)]
45/// struct MyAppState {
46///     db: sqlx::PgPool,
47///     cookie_key: CookieKey,
48/// }
49///
50/// impl AppState for MyAppState {
51///     fn version(&self) -> &str { "1.0.0" }
52///     fn db(&self) -> &sqlx::PgPool { &self.db }
53///     fn cookie_key(&self) -> &CookieKey { &self.cookie_key }
54/// }
55///
56/// // Define your job types
57/// #[derive(Debug, Serialize, Deserialize, Clone)]
58/// struct ProcessPaymentJob {
59///     user_id: i32,
60///     amount_cents: i64,
61/// }
62///
63/// #[derive(Debug, Serialize, Deserialize, Clone)]
64/// struct SendNotificationJob {
65///     user_id: i32,
66///     message: String,
67/// }
68///
69/// // Implement the Job trait for each job type
70/// #[async_trait::async_trait]
71/// impl Job<MyAppState> for ProcessPaymentJob {
72///     const NAME: &'static str = "ProcessPaymentJob";
73///     async fn run(&self, _: MyAppState) -> color_eyre::Result<()> {
74///         println!("Processing payment for user {}", self.user_id);
75///         Ok(())
76///     }
77/// }
78///
79/// #[async_trait::async_trait]
80/// impl Job<MyAppState> for SendNotificationJob {
81///     const NAME: &'static str = "SendNotificationJob";
82///     async fn run(&self, _: MyAppState) -> color_eyre::Result<()> {
83///         println!("Sending notification to user {}: {}", self.user_id, self.message);
84///         Ok(())
85///     }
86/// }
87///
88/// // Register all your job types with the macro
89/// impl_job_registry!(MyAppState, ProcessPaymentJob, SendNotificationJob);
90/// ```
91#[macro_export]
92macro_rules! impl_job_registry {
93    ($state:ty, $($job_type:ty),*) => {
94        pub struct Jobs;
95
96        #[async_trait::async_trait]
97        impl $crate::jobs::registry::JobRegistry<$state> for Jobs {
98            async fn run_job(
99                &self,
100                job: &$crate::jobs::worker::JobFromDB,
101                app_state: $state,
102                cancellation_token: $crate::jobs::CancellationToken,
103            ) -> $crate::Result<()> {
104                use $crate::jobs::Job as _;
105
106                let payload = job.payload.clone();
107
108                match job.name.as_str() {
109                    $(
110                        <$job_type>::NAME => <$job_type>::run_from_value(payload, app_state, cancellation_token).await,
111                    )*
112                    _ => Err($crate::color_eyre::eyre::eyre!("Unknown job type: {}", job.name)),
113                }
114            }
115
116            fn job_names() -> &'static [&'static str] {
117                use $crate::jobs::Job as _;
118
119                &[$(<$job_type>::NAME),*]
120            }
121        }
122    };
123}
124
125#[cfg(test)]
126mod test {
127    use crate::app_state::AppState;
128    use crate::jobs::Job;
129    use crate::server::cookies::CookieKey;
130
131    #[derive(Clone)]
132    struct TestAppState {
133        db: sqlx::PgPool,
134        cookie_key: CookieKey,
135    }
136
137    impl AppState for TestAppState {
138        fn db(&self) -> &sqlx::PgPool {
139            &self.db
140        }
141
142        fn version(&self) -> &'static str {
143            "test"
144        }
145
146        fn cookie_key(&self) -> &CookieKey {
147            &self.cookie_key
148        }
149    }
150
151    #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
152    struct FirstJob;
153
154    #[async_trait::async_trait]
155    impl Job<TestAppState> for FirstJob {
156        const NAME: &'static str = "FirstJob";
157
158        async fn run(&self, _app_state: TestAppState) -> color_eyre::Result<()> {
159            Ok(())
160        }
161    }
162
163    #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
164    struct SecondJob;
165
166    #[async_trait::async_trait]
167    impl Job<TestAppState> for SecondJob {
168        const NAME: &'static str = "SecondJob";
169
170        async fn run(&self, _app_state: TestAppState) -> color_eyre::Result<()> {
171            Ok(())
172        }
173    }
174
175    impl_job_registry!(TestAppState, FirstJob, SecondJob);
176
177    #[test]
178    fn test_job_names_lists_all_registered_jobs() {
179        use crate::jobs::registry::JobRegistry;
180
181        let names = <Jobs as JobRegistry<TestAppState>>::job_names();
182        assert_eq!(names, &["FirstJob", "SecondJob"]);
183    }
184}