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
11pub struct JobContext {
18 pub(crate) registry: Arc<RegistrySnapshot>,
19 pub(crate) payload: String,
20 pub(crate) meta: Meta,
21}
22
23pub trait FromJobContext: Sized {
32 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}