lab_resource_manager/infrastructure/notifier/
router.rs

1use crate::domain::ports::notifier::{NotificationError, NotificationEvent, Notifier};
2use crate::domain::ports::repositories::IdentityLinkRepository;
3use crate::infrastructure::config::{NotificationConfig, ResourceConfig};
4use async_trait::async_trait;
5use std::collections::HashSet;
6use std::sync::Arc;
7
8use super::senders::{
9    MockSender, SlackSender,
10    sender::{NotificationContext, Sender},
11};
12
13/// 複数の通知手段をオーケストレートし、リソースに基づいて適切な通知先にルーティングする
14///
15/// 各種Sender(Slack, Mock等)を保持し、通知設定の種類に応じて適切なSenderに委譲します。
16pub struct NotificationRouter {
17    config: ResourceConfig,
18    slack_sender: SlackSender,
19    mock_sender: MockSender,
20    identity_repo: Arc<dyn IdentityLinkRepository>,
21}
22
23impl NotificationRouter {
24    pub fn new(config: ResourceConfig, identity_repo: Arc<dyn IdentityLinkRepository>) -> Self {
25        Self {
26            config,
27            slack_sender: SlackSender::new(),
28            mock_sender: MockSender::new(),
29            identity_repo,
30        }
31    }
32
33    fn collect_notification_configs(&self, event: &NotificationEvent) -> Vec<NotificationConfig> {
34        let resources = match event {
35            NotificationEvent::ResourceUsageCreated(usage) => usage.resources(),
36            NotificationEvent::ResourceUsageUpdated(usage) => usage.resources(),
37            NotificationEvent::ResourceUsageDeleted(usage) => usage.resources(),
38        };
39
40        let mut configs = HashSet::new();
41        for resource in resources {
42            let resource_configs = self.config.get_notifications_for_resource(resource);
43            configs.extend(resource_configs);
44        }
45
46        configs.into_iter().collect()
47    }
48
49    async fn send_to_destination(
50        &self,
51        config: &NotificationConfig,
52        event: &NotificationEvent,
53    ) -> Result<(), NotificationError> {
54        let usage = match event {
55            NotificationEvent::ResourceUsageCreated(u) => u,
56            NotificationEvent::ResourceUsageUpdated(u) => u,
57            NotificationEvent::ResourceUsageDeleted(u) => u,
58        };
59
60        let user_email = usage.owner_email();
61
62        // IdentityLinkを取得
63        // Ok(None) = IdentityLinkが未登録(正常ケース)
64        // Ok(Some(_)) = IdentityLinkが存在
65        // Err(_) = リポジトリエラー(DB接続障害等の異常ケース)
66        let identity_link = self
67            .identity_repo
68            .find_by_email(user_email)
69            .await
70            .map_err(|e| {
71                NotificationError::RepositoryError(format!(
72                    "IdentityLink取得失敗 (email: {}): {}",
73                    user_email.as_str(),
74                    e
75                ))
76            })?;
77
78        let context = NotificationContext {
79            event,
80            identity_link: identity_link.as_ref(),
81        };
82
83        match config {
84            NotificationConfig::Slack { webhook_url } => {
85                self.slack_sender.send(webhook_url.as_str(), context).await
86            }
87            NotificationConfig::Mock {} => self.mock_sender.send(&(), context).await,
88        }
89    }
90}
91
92#[async_trait]
93impl Notifier for NotificationRouter {
94    async fn notify(&self, event: NotificationEvent) -> Result<(), NotificationError> {
95        let notification_configs = self.collect_notification_configs(&event);
96
97        if notification_configs.is_empty() {
98            // 通知先が設定されていない場合は何もしない
99            return Ok(());
100        }
101
102        let mut errors = Vec::new();
103
104        // 各通知設定に対して送信(ベストエフォート)
105        for config in &notification_configs {
106            if let Err(e) = self.send_to_destination(config, &event).await {
107                eprintln!("⚠️  通知送信エラー: {}", e); // TODO: エラーハンドリングの改善
108                errors.push(e);
109            }
110        }
111
112        Ok(())
113    }
114}