1use serde::de::DeserializeOwned;
2use std::sync::Arc;
3use tokio_util::sync::CancellationToken;
4use uuid::Uuid;
5
6use crate::config::{ConfigError, ConfigProvider, module_config_or_default};
8
9#[derive(Clone)]
12#[must_use]
13pub struct ModuleCtx {
14 module_name: Arc<str>,
15 instance_id: Uuid,
16 config_provider: Arc<dyn ConfigProvider>,
17 client_hub: Arc<crate::client_hub::ClientHub>,
18 cancellation_token: CancellationToken,
19 db_handle: Option<Arc<modkit_db::DbHandle>>,
20}
21
22pub struct ModuleContextBuilder {
27 instance_id: Uuid,
28 config_provider: Arc<dyn ConfigProvider>,
29 client_hub: Arc<crate::client_hub::ClientHub>,
30 root_token: CancellationToken,
31 db_manager: Option<Arc<modkit_db::DbManager>>, }
33
34impl ModuleContextBuilder {
35 pub fn new(
36 instance_id: Uuid,
37 config_provider: Arc<dyn ConfigProvider>,
38 client_hub: Arc<crate::client_hub::ClientHub>,
39 root_token: CancellationToken,
40 db_manager: Option<Arc<modkit_db::DbManager>>,
41 ) -> Self {
42 Self {
43 instance_id,
44 config_provider,
45 client_hub,
46 root_token,
47 db_manager,
48 }
49 }
50
51 #[must_use]
53 pub fn instance_id(&self) -> Uuid {
54 self.instance_id
55 }
56
57 pub async fn for_module(&self, module_name: &str) -> anyhow::Result<ModuleCtx> {
62 let db_handle = if let Some(mgr) = &self.db_manager {
63 mgr.get(module_name).await?
64 } else {
65 None
66 };
67
68 Ok(ModuleCtx::new(
69 Arc::<str>::from(module_name),
70 self.instance_id,
71 self.config_provider.clone(),
72 self.client_hub.clone(),
73 self.root_token.child_token(),
74 db_handle,
75 ))
76 }
77}
78
79impl ModuleCtx {
80 pub fn new(
82 module_name: impl Into<Arc<str>>,
83 instance_id: Uuid,
84 config_provider: Arc<dyn ConfigProvider>,
85 client_hub: Arc<crate::client_hub::ClientHub>,
86 cancellation_token: CancellationToken,
87 db_handle: Option<Arc<modkit_db::DbHandle>>,
88 ) -> Self {
89 Self {
90 module_name: module_name.into(),
91 instance_id,
92 config_provider,
93 client_hub,
94 cancellation_token,
95 db_handle,
96 }
97 }
98
99 #[inline]
102 #[must_use]
103 pub fn module_name(&self) -> &str {
104 &self.module_name
105 }
106
107 #[inline]
112 #[must_use]
113 pub fn instance_id(&self) -> Uuid {
114 self.instance_id
115 }
116
117 #[inline]
118 #[must_use]
119 pub fn config_provider(&self) -> &dyn ConfigProvider {
120 &*self.config_provider
121 }
122
123 #[inline]
125 #[must_use]
126 pub fn client_hub(&self) -> Arc<crate::client_hub::ClientHub> {
127 self.client_hub.clone()
128 }
129
130 #[inline]
131 #[must_use]
132 pub fn cancellation_token(&self) -> &CancellationToken {
133 &self.cancellation_token
134 }
135
136 #[must_use]
137 pub fn db_optional(&self) -> Option<Arc<modkit_db::DbHandle>> {
138 self.db_handle.clone()
139 }
140
141 pub fn db_required(&self) -> anyhow::Result<Arc<modkit_db::DbHandle>> {
146 self.db_handle.clone().ok_or_else(|| {
147 anyhow::anyhow!(
148 "Database is not configured for module '{}'",
149 self.module_name
150 )
151 })
152 }
153
154 #[must_use]
155 pub fn current_module(&self) -> Option<&str> {
156 Some(&self.module_name)
157 }
158
159 pub fn config<T: DeserializeOwned + Default>(&self) -> Result<T, ConfigError> {
182 module_config_or_default(self.config_provider.as_ref(), &self.module_name)
183 }
184
185 #[must_use]
188 pub fn raw_config(&self) -> &serde_json::Value {
189 use std::sync::LazyLock;
190
191 static EMPTY: LazyLock<serde_json::Value> =
192 LazyLock::new(|| serde_json::Value::Object(serde_json::Map::new()));
193
194 if let Some(module_raw) = self.config_provider.get_module_config(&self.module_name) {
195 if let Some(obj) = module_raw.as_object()
197 && let Some(config_section) = obj.get("config")
198 {
199 return config_section;
200 }
201 }
202 &EMPTY
203 }
204
205 pub fn with_db(&self, db: Arc<modkit_db::DbHandle>) -> ModuleCtx {
208 ModuleCtx {
209 module_name: self.module_name.clone(),
210 instance_id: self.instance_id,
211 config_provider: self.config_provider.clone(),
212 client_hub: self.client_hub.clone(),
213 cancellation_token: self.cancellation_token.clone(),
214 db_handle: Some(db),
215 }
216 }
217
218 pub fn without_db(&self) -> ModuleCtx {
221 ModuleCtx {
222 module_name: self.module_name.clone(),
223 instance_id: self.instance_id,
224 config_provider: self.config_provider.clone(),
225 client_hub: self.client_hub.clone(),
226 cancellation_token: self.cancellation_token.clone(),
227 db_handle: None,
228 }
229 }
230}
231
232#[cfg(test)]
233#[cfg_attr(coverage_nightly, coverage(off))]
234mod tests {
235 use super::*;
236 use serde::Deserialize;
237 use serde_json::json;
238 use std::collections::HashMap;
239
240 #[derive(Debug, PartialEq, Deserialize, Default)]
241 struct TestConfig {
242 #[serde(default)]
243 api_key: String,
244 #[serde(default)]
245 timeout_ms: u64,
246 #[serde(default)]
247 enabled: bool,
248 }
249
250 struct MockConfigProvider {
251 modules: HashMap<String, serde_json::Value>,
252 }
253
254 impl MockConfigProvider {
255 fn new() -> Self {
256 let mut modules = HashMap::new();
257
258 modules.insert(
260 "test_module".to_owned(),
261 json!({
262 "database": {
263 "url": "postgres://localhost/test"
264 },
265 "config": {
266 "api_key": "secret123",
267 "timeout_ms": 5000,
268 "enabled": true
269 }
270 }),
271 );
272
273 Self { modules }
274 }
275 }
276
277 impl ConfigProvider for MockConfigProvider {
278 fn get_module_config(&self, module_name: &str) -> Option<&serde_json::Value> {
279 self.modules.get(module_name)
280 }
281 }
282
283 #[test]
284 fn test_module_ctx_config_with_valid_config() {
285 let provider = Arc::new(MockConfigProvider::new());
286 let ctx = ModuleCtx::new(
287 "test_module",
288 Uuid::new_v4(),
289 provider,
290 Arc::new(crate::client_hub::ClientHub::default()),
291 CancellationToken::new(),
292 None,
293 );
294
295 let result: Result<TestConfig, ConfigError> = ctx.config();
296 assert!(result.is_ok());
297
298 let config = result.unwrap();
299 assert_eq!(config.api_key, "secret123");
300 assert_eq!(config.timeout_ms, 5000);
301 assert!(config.enabled);
302 }
303
304 #[test]
305 fn test_module_ctx_config_returns_default_for_missing_module() {
306 let provider = Arc::new(MockConfigProvider::new());
307 let ctx = ModuleCtx::new(
308 "nonexistent_module",
309 Uuid::new_v4(),
310 provider,
311 Arc::new(crate::client_hub::ClientHub::default()),
312 CancellationToken::new(),
313 None,
314 );
315
316 let result: Result<TestConfig, ConfigError> = ctx.config();
317 assert!(result.is_ok());
318
319 let config = result.unwrap();
320 assert_eq!(config, TestConfig::default());
321 }
322
323 #[test]
324 fn test_module_ctx_instance_id() {
325 let provider = Arc::new(MockConfigProvider::new());
326 let instance_id = Uuid::new_v4();
327 let ctx = ModuleCtx::new(
328 "test_module",
329 instance_id,
330 provider,
331 Arc::new(crate::client_hub::ClientHub::default()),
332 CancellationToken::new(),
333 None,
334 );
335
336 assert_eq!(ctx.instance_id(), instance_id);
337 }
338}