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}