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#[cfg(feature = "db")]
14pub(crate) type DbManager = modkit_db::DbManager;
15#[cfg(feature = "db")]
16pub(crate) type DbProvider = modkit_db::DBProvider<modkit_db::DbError>;
17
18#[cfg(not(feature = "db"))]
20#[derive(Clone, Debug)]
21pub struct DbManager;
22#[cfg(not(feature = "db"))]
23#[derive(Clone, Debug)]
24pub struct DbProvider;
25
26#[derive(Clone)]
27#[must_use]
28pub struct ModuleCtx {
29 module_name: Arc<str>,
30 instance_id: Uuid,
31 config_provider: Arc<dyn ConfigProvider>,
32 client_hub: Arc<crate::client_hub::ClientHub>,
33 cancellation_token: CancellationToken,
34 db: Option<DbProvider>,
35}
36
37pub struct ModuleContextBuilder {
42 instance_id: Uuid,
43 config_provider: Arc<dyn ConfigProvider>,
44 client_hub: Arc<crate::client_hub::ClientHub>,
45 root_token: CancellationToken,
46 db_manager: Option<Arc<DbManager>>, }
48
49impl ModuleContextBuilder {
50 pub fn new(
51 instance_id: Uuid,
52 config_provider: Arc<dyn ConfigProvider>,
53 client_hub: Arc<crate::client_hub::ClientHub>,
54 root_token: CancellationToken,
55 db_manager: Option<Arc<DbManager>>,
56 ) -> Self {
57 Self {
58 instance_id,
59 config_provider,
60 client_hub,
61 root_token,
62 db_manager,
63 }
64 }
65
66 #[must_use]
68 pub fn instance_id(&self) -> Uuid {
69 self.instance_id
70 }
71
72 pub async fn for_module(&self, module_name: &str) -> anyhow::Result<ModuleCtx> {
77 let db: Option<DbProvider> = {
78 #[cfg(feature = "db")]
79 {
80 if let Some(mgr) = &self.db_manager {
81 mgr.get(module_name).await?.map(modkit_db::DBProvider::new)
82 } else {
83 None
84 }
85 }
86 #[cfg(not(feature = "db"))]
87 {
88 let _ = module_name; None
90 }
91 };
92
93 Ok(ModuleCtx::new(
94 Arc::<str>::from(module_name),
95 self.instance_id,
96 self.config_provider.clone(),
97 self.client_hub.clone(),
98 self.root_token.child_token(),
99 db,
100 ))
101 }
102}
103
104impl ModuleCtx {
105 pub fn new(
107 module_name: impl Into<Arc<str>>,
108 instance_id: Uuid,
109 config_provider: Arc<dyn ConfigProvider>,
110 client_hub: Arc<crate::client_hub::ClientHub>,
111 cancellation_token: CancellationToken,
112 db: Option<DbProvider>,
113 ) -> Self {
114 Self {
115 module_name: module_name.into(),
116 instance_id,
117 config_provider,
118 client_hub,
119 cancellation_token,
120 db,
121 }
122 }
123
124 #[inline]
127 #[must_use]
128 pub fn module_name(&self) -> &str {
129 &self.module_name
130 }
131
132 #[inline]
137 #[must_use]
138 pub fn instance_id(&self) -> Uuid {
139 self.instance_id
140 }
141
142 #[inline]
143 #[must_use]
144 pub fn config_provider(&self) -> &dyn ConfigProvider {
145 &*self.config_provider
146 }
147
148 #[inline]
150 #[must_use]
151 pub fn client_hub(&self) -> Arc<crate::client_hub::ClientHub> {
152 self.client_hub.clone()
153 }
154
155 #[inline]
156 #[must_use]
157 pub fn cancellation_token(&self) -> &CancellationToken {
158 &self.cancellation_token
159 }
160
161 #[must_use]
180 #[cfg(feature = "db")]
181 pub fn db(&self) -> Option<modkit_db::DBProvider<modkit_db::DbError>> {
182 self.db.clone()
183 }
184
185 #[cfg(feature = "db")]
202 pub fn db_required(&self) -> anyhow::Result<modkit_db::DBProvider<modkit_db::DbError>> {
203 self.db().ok_or_else(|| {
204 anyhow::anyhow!(
205 "Database is not configured for module '{}'",
206 self.module_name
207 )
208 })
209 }
210
211 pub fn config<T: DeserializeOwned + Default>(&self) -> Result<T, ConfigError> {
234 module_config_or_default(self.config_provider.as_ref(), &self.module_name)
235 }
236
237 #[must_use]
240 pub fn raw_config(&self) -> &serde_json::Value {
241 use std::sync::LazyLock;
242
243 static EMPTY: LazyLock<serde_json::Value> =
244 LazyLock::new(|| serde_json::Value::Object(serde_json::Map::new()));
245
246 if let Some(module_raw) = self.config_provider.get_module_config(&self.module_name) {
247 if let Some(obj) = module_raw.as_object()
249 && let Some(config_section) = obj.get("config")
250 {
251 return config_section;
252 }
253 }
254 &EMPTY
255 }
256
257 pub fn without_db(&self) -> ModuleCtx {
260 ModuleCtx {
261 module_name: self.module_name.clone(),
262 instance_id: self.instance_id,
263 config_provider: self.config_provider.clone(),
264 client_hub: self.client_hub.clone(),
265 cancellation_token: self.cancellation_token.clone(),
266 db: None,
267 }
268 }
269}
270
271#[cfg(test)]
272#[cfg_attr(coverage_nightly, coverage(off))]
273mod tests {
274 use super::*;
275 use serde::Deserialize;
276 use serde_json::json;
277 use std::collections::HashMap;
278
279 #[derive(Debug, PartialEq, Deserialize, Default)]
280 struct TestConfig {
281 #[serde(default)]
282 api_key: String,
283 #[serde(default)]
284 timeout_ms: u64,
285 #[serde(default)]
286 enabled: bool,
287 }
288
289 struct MockConfigProvider {
290 modules: HashMap<String, serde_json::Value>,
291 }
292
293 impl MockConfigProvider {
294 fn new() -> Self {
295 let mut modules = HashMap::new();
296
297 modules.insert(
299 "test_module".to_owned(),
300 json!({
301 "database": {
302 "url": "postgres://localhost/test"
303 },
304 "config": {
305 "api_key": "secret123",
306 "timeout_ms": 5000,
307 "enabled": true
308 }
309 }),
310 );
311
312 Self { modules }
313 }
314 }
315
316 impl ConfigProvider for MockConfigProvider {
317 fn get_module_config(&self, module_name: &str) -> Option<&serde_json::Value> {
318 self.modules.get(module_name)
319 }
320 }
321
322 #[test]
323 fn test_module_ctx_config_with_valid_config() {
324 let provider = Arc::new(MockConfigProvider::new());
325 let ctx = ModuleCtx::new(
326 "test_module",
327 Uuid::new_v4(),
328 provider,
329 Arc::new(crate::client_hub::ClientHub::default()),
330 CancellationToken::new(),
331 None,
332 );
333
334 let result: Result<TestConfig, ConfigError> = ctx.config();
335 assert!(result.is_ok());
336
337 let config = result.unwrap();
338 assert_eq!(config.api_key, "secret123");
339 assert_eq!(config.timeout_ms, 5000);
340 assert!(config.enabled);
341 }
342
343 #[test]
344 fn test_module_ctx_config_returns_default_for_missing_module() {
345 let provider = Arc::new(MockConfigProvider::new());
346 let ctx = ModuleCtx::new(
347 "nonexistent_module",
348 Uuid::new_v4(),
349 provider,
350 Arc::new(crate::client_hub::ClientHub::default()),
351 CancellationToken::new(),
352 None,
353 );
354
355 let result: Result<TestConfig, ConfigError> = ctx.config();
356 assert!(result.is_ok());
357
358 let config = result.unwrap();
359 assert_eq!(config, TestConfig::default());
360 }
361
362 #[test]
363 fn test_module_ctx_instance_id() {
364 let provider = Arc::new(MockConfigProvider::new());
365 let instance_id = Uuid::new_v4();
366 let ctx = ModuleCtx::new(
367 "test_module",
368 instance_id,
369 provider,
370 Arc::new(crate::client_hub::ClientHub::default()),
371 CancellationToken::new(),
372 None,
373 );
374
375 assert_eq!(ctx.instance_id(), instance_id);
376 }
377}