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 #[must_use]
212 pub fn current_module(&self) -> Option<&str> {
213 Some(&self.module_name)
214 }
215
216 pub fn config<T: DeserializeOwned + Default>(&self) -> Result<T, ConfigError> {
239 module_config_or_default(self.config_provider.as_ref(), &self.module_name)
240 }
241
242 #[must_use]
245 pub fn raw_config(&self) -> &serde_json::Value {
246 use std::sync::LazyLock;
247
248 static EMPTY: LazyLock<serde_json::Value> =
249 LazyLock::new(|| serde_json::Value::Object(serde_json::Map::new()));
250
251 if let Some(module_raw) = self.config_provider.get_module_config(&self.module_name) {
252 if let Some(obj) = module_raw.as_object()
254 && let Some(config_section) = obj.get("config")
255 {
256 return config_section;
257 }
258 }
259 &EMPTY
260 }
261
262 pub fn without_db(&self) -> ModuleCtx {
265 ModuleCtx {
266 module_name: self.module_name.clone(),
267 instance_id: self.instance_id,
268 config_provider: self.config_provider.clone(),
269 client_hub: self.client_hub.clone(),
270 cancellation_token: self.cancellation_token.clone(),
271 db: None,
272 }
273 }
274}
275
276#[cfg(test)]
277#[cfg_attr(coverage_nightly, coverage(off))]
278mod tests {
279 use super::*;
280 use serde::Deserialize;
281 use serde_json::json;
282 use std::collections::HashMap;
283
284 #[derive(Debug, PartialEq, Deserialize, Default)]
285 struct TestConfig {
286 #[serde(default)]
287 api_key: String,
288 #[serde(default)]
289 timeout_ms: u64,
290 #[serde(default)]
291 enabled: bool,
292 }
293
294 struct MockConfigProvider {
295 modules: HashMap<String, serde_json::Value>,
296 }
297
298 impl MockConfigProvider {
299 fn new() -> Self {
300 let mut modules = HashMap::new();
301
302 modules.insert(
304 "test_module".to_owned(),
305 json!({
306 "database": {
307 "url": "postgres://localhost/test"
308 },
309 "config": {
310 "api_key": "secret123",
311 "timeout_ms": 5000,
312 "enabled": true
313 }
314 }),
315 );
316
317 Self { modules }
318 }
319 }
320
321 impl ConfigProvider for MockConfigProvider {
322 fn get_module_config(&self, module_name: &str) -> Option<&serde_json::Value> {
323 self.modules.get(module_name)
324 }
325 }
326
327 #[test]
328 fn test_module_ctx_config_with_valid_config() {
329 let provider = Arc::new(MockConfigProvider::new());
330 let ctx = ModuleCtx::new(
331 "test_module",
332 Uuid::new_v4(),
333 provider,
334 Arc::new(crate::client_hub::ClientHub::default()),
335 CancellationToken::new(),
336 None,
337 );
338
339 let result: Result<TestConfig, ConfigError> = ctx.config();
340 assert!(result.is_ok());
341
342 let config = result.unwrap();
343 assert_eq!(config.api_key, "secret123");
344 assert_eq!(config.timeout_ms, 5000);
345 assert!(config.enabled);
346 }
347
348 #[test]
349 fn test_module_ctx_config_returns_default_for_missing_module() {
350 let provider = Arc::new(MockConfigProvider::new());
351 let ctx = ModuleCtx::new(
352 "nonexistent_module",
353 Uuid::new_v4(),
354 provider,
355 Arc::new(crate::client_hub::ClientHub::default()),
356 CancellationToken::new(),
357 None,
358 );
359
360 let result: Result<TestConfig, ConfigError> = ctx.config();
361 assert!(result.is_ok());
362
363 let config = result.unwrap();
364 assert_eq!(config, TestConfig::default());
365 }
366
367 #[test]
368 fn test_module_ctx_instance_id() {
369 let provider = Arc::new(MockConfigProvider::new());
370 let instance_id = Uuid::new_v4();
371 let ctx = ModuleCtx::new(
372 "test_module",
373 instance_id,
374 provider,
375 Arc::new(crate::client_hub::ClientHub::default()),
376 CancellationToken::new(),
377 None,
378 );
379
380 assert_eq!(ctx.instance_id(), instance_id);
381 }
382}