Skip to main content

modo/job/
context.rs

1use std::sync::Arc;
2
3use serde::de::DeserializeOwned;
4
5use crate::error::{Error, Result};
6use crate::service::{RegistrySnapshot, Service};
7
8use super::meta::Meta;
9use super::payload::Payload;
10
11/// Runtime context passed to every job handler invocation.
12///
13/// `JobContext` carries the raw JSON payload, the job metadata ([`Meta`]), and
14/// a snapshot of the service registry. Handler arguments that implement
15/// [`FromJobContext`] are extracted from this context automatically before the
16/// handler is called.
17pub struct JobContext {
18    pub(crate) registry: Arc<RegistrySnapshot>,
19    pub(crate) payload: String,
20    pub(crate) meta: Meta,
21}
22
23/// Extraction trait for job handler arguments.
24///
25/// Implement this trait to define custom types that can appear as parameters
26/// in job handler functions. Three implementations are provided out of the box:
27///
28/// - [`Payload<T>`] — deserializes the JSON payload into `T`
29/// - [`Service<T>`] — retrieves a service from the registry
30/// - [`Meta`] — returns a clone of the job metadata
31pub trait FromJobContext: Sized {
32    /// Extract `Self` from the job context, returning an error if extraction
33    /// fails.
34    ///
35    /// # Errors
36    ///
37    /// Returns an error if extraction fails (e.g. payload deserialization
38    /// error or missing service in the registry).
39    fn from_job_context(ctx: &JobContext) -> Result<Self>;
40}
41
42impl<T: DeserializeOwned> FromJobContext for Payload<T> {
43    fn from_job_context(ctx: &JobContext) -> Result<Self> {
44        let value: T = serde_json::from_str(&ctx.payload).map_err(|e| {
45            Error::internal(format!(
46                "failed to deserialize job payload for '{}': {e}",
47                ctx.meta.name
48            ))
49        })?;
50        Ok(Payload(value))
51    }
52}
53
54impl<T: Send + Sync + 'static> FromJobContext for Service<T> {
55    fn from_job_context(ctx: &JobContext) -> Result<Self> {
56        ctx.registry.get::<T>().map(Service).ok_or_else(|| {
57            Error::internal(format!(
58                "service not found in registry: {}",
59                std::any::type_name::<T>()
60            ))
61        })
62    }
63}
64
65impl FromJobContext for Meta {
66    fn from_job_context(ctx: &JobContext) -> Result<Self> {
67        Ok(ctx.meta.clone())
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use std::any::{Any, TypeId};
75    use std::collections::HashMap;
76
77    fn test_context(payload: &str) -> JobContext {
78        let mut services: HashMap<TypeId, Arc<dyn Any + Send + Sync>> = HashMap::new();
79        services.insert(TypeId::of::<String>(), Arc::new("test-service".to_string()));
80        let snapshot = Arc::new(RegistrySnapshot::new(services));
81
82        JobContext {
83            registry: snapshot,
84            payload: payload.to_string(),
85            meta: Meta {
86                id: "test-id".to_string(),
87                name: "test-job".to_string(),
88                queue: "default".to_string(),
89                attempt: 1,
90                max_attempts: 3,
91                deadline: None,
92            },
93        }
94    }
95
96    #[test]
97    fn payload_extractor_deserializes_json() {
98        let ctx = test_context(r#"{"value": 42}"#);
99
100        #[derive(serde::Deserialize)]
101        struct TestPayload {
102            value: u32,
103        }
104
105        let payload = Payload::<TestPayload>::from_job_context(&ctx).unwrap();
106        assert_eq!(payload.value, 42);
107    }
108
109    #[test]
110    fn payload_extractor_fails_on_invalid_json() {
111        let ctx = test_context("not json");
112        let result = Payload::<serde_json::Value>::from_job_context(&ctx);
113        assert!(result.is_err());
114    }
115
116    #[test]
117    fn service_extractor_finds_registered() {
118        let ctx = test_context("{}");
119        let svc = Service::<String>::from_job_context(&ctx).unwrap();
120        assert_eq!(*svc.0, "test-service");
121    }
122
123    #[test]
124    fn service_extractor_fails_for_missing() {
125        let ctx = test_context("{}");
126        let result = Service::<u64>::from_job_context(&ctx);
127        assert!(result.is_err());
128    }
129
130    #[test]
131    fn meta_extractor_clones_meta() {
132        let ctx = test_context("{}");
133        let meta = Meta::from_job_context(&ctx).unwrap();
134        assert_eq!(meta.id, "test-id");
135        assert_eq!(meta.name, "test-job");
136        assert_eq!(meta.attempt, 1);
137    }
138}