Skip to main content

a2a_rs/port/
notification_manager.rs

1//! Push notification management port definitions
2
3#[cfg(feature = "server")]
4use async_trait::async_trait;
5
6use crate::domain::{A2AError, PushNotificationConfig, TaskIdParams, TaskPushNotificationConfig};
7
8/// Validate a push notification config URL.
9///
10/// Checks that the URL is non-empty, well-formed, and uses HTTPS
11/// (HTTP is allowed only for localhost for development purposes).
12fn validate_push_notification_url(config: &PushNotificationConfig) -> Result<(), A2AError> {
13    if config.url.trim().is_empty() {
14        return Err(A2AError::ValidationError {
15            field: "url".to_string(),
16            message: "Webhook URL cannot be empty".to_string(),
17        });
18    }
19
20    match url::Url::parse(&config.url) {
21        Ok(parsed_url) => {
22            let scheme = parsed_url.scheme();
23            if scheme != "https" {
24                let is_localhost = parsed_url
25                    .host_str()
26                    .map(|h| h == "localhost" || h == "127.0.0.1" || h == "::1")
27                    .unwrap_or(false);
28
29                if scheme != "http" || !is_localhost {
30                    return Err(A2AError::ValidationError {
31                        field: "url".to_string(),
32                        message: "Webhook URL must use HTTPS (HTTP is only allowed for localhost)"
33                            .to_string(),
34                    });
35                }
36            }
37        }
38        Err(_) => {
39            return Err(A2AError::ValidationError {
40                field: "url".to_string(),
41                message: "Invalid webhook URL format".to_string(),
42            });
43        }
44    }
45
46    Ok(())
47}
48
49/// A trait for managing push notification configurations and delivery
50pub trait NotificationManager {
51    /// Set up push notifications for a task
52    fn set_task_notification(
53        &self,
54        config: &TaskPushNotificationConfig,
55    ) -> Result<TaskPushNotificationConfig, A2AError>;
56
57    /// Get the push notification configuration for a task
58    fn get_task_notification(&self, task_id: &str) -> Result<TaskPushNotificationConfig, A2AError>;
59
60    /// Remove push notification configuration for a task
61    fn remove_task_notification(&self, task_id: &str) -> Result<(), A2AError>;
62
63    /// Check if push notifications are configured for a task
64    fn has_task_notification(&self, task_id: &str) -> Result<bool, A2AError> {
65        match self.get_task_notification(task_id) {
66            Ok(_) => Ok(true),
67            Err(A2AError::TaskNotFound(_)) => Ok(false),
68            Err(e) => Err(e),
69        }
70    }
71
72    /// Validate push notification configuration
73    fn validate_notification_config(
74        &self,
75        config: &PushNotificationConfig,
76    ) -> Result<(), A2AError> {
77        validate_push_notification_url(config)
78    }
79
80    /// Send a test notification to verify configuration
81    fn send_test_notification(&self, config: &PushNotificationConfig) -> Result<(), A2AError> {
82        // Default implementation - can be overridden
83        self.validate_notification_config(config)?;
84        // In a real implementation, this would send a test notification
85        Ok(())
86    }
87}
88
89#[cfg(feature = "server")]
90#[async_trait]
91/// An async trait for managing push notification configurations and delivery
92pub trait AsyncNotificationManager: Send + Sync {
93    /// Set up push notifications for a task
94    async fn set_task_notification(
95        &self,
96        config: &TaskPushNotificationConfig,
97    ) -> Result<TaskPushNotificationConfig, A2AError>;
98
99    /// Get the push notification configuration for a task
100    async fn get_task_notification(
101        &self,
102        task_id: &str,
103    ) -> Result<TaskPushNotificationConfig, A2AError>;
104
105    /// Remove push notification configuration for a task
106    async fn remove_task_notification(&self, task_id: &str) -> Result<(), A2AError>;
107
108    /// Check if push notifications are configured for a task
109    async fn has_task_notification(&self, task_id: &str) -> Result<bool, A2AError> {
110        match self.get_task_notification(task_id).await {
111            Ok(_) => Ok(true),
112            Err(A2AError::TaskNotFound(_)) => Ok(false),
113            Err(e) => Err(e),
114        }
115    }
116
117    /// Validate push notification configuration
118    async fn validate_notification_config(
119        &self,
120        config: &PushNotificationConfig,
121    ) -> Result<(), A2AError> {
122        validate_push_notification_url(config)
123    }
124
125    /// Send a test notification to verify configuration
126    async fn send_test_notification(
127        &self,
128        config: &PushNotificationConfig,
129    ) -> Result<(), A2AError> {
130        // Default implementation - can be overridden
131        self.validate_notification_config(config).await?;
132        // In a real implementation, this would send a test notification
133        Ok(())
134    }
135
136    /// Set task notification with validation
137    async fn set_task_notification_validated(
138        &self,
139        config: &TaskPushNotificationConfig,
140    ) -> Result<TaskPushNotificationConfig, A2AError> {
141        // Validate the task ID
142        if config.task_id.trim().is_empty() {
143            return Err(A2AError::ValidationError {
144                field: "task_id".to_string(),
145                message: "Task ID cannot be empty".to_string(),
146            });
147        }
148
149        // Validate the notification config
150        self.validate_notification_config(&config.push_notification_config)
151            .await?;
152
153        // Set the notification
154        self.set_task_notification(config).await
155    }
156
157    /// Get task notification with validation
158    async fn get_task_notification_validated(
159        &self,
160        params: &TaskIdParams,
161    ) -> Result<TaskPushNotificationConfig, A2AError> {
162        if params.id.trim().is_empty() {
163            return Err(A2AError::ValidationError {
164                field: "task_id".to_string(),
165                message: "Task ID cannot be empty".to_string(),
166            });
167        }
168
169        self.get_task_notification(&params.id).await
170    }
171
172    /// Send notification for task status update
173    async fn notify_task_status_update(
174        &self,
175        task_id: &str,
176        _status_update: &crate::domain::TaskStatusUpdateEvent,
177    ) -> Result<(), A2AError> {
178        // Default implementation - can be overridden
179        // Check if notifications are configured for this task
180        if !self.has_task_notification(task_id).await? {
181            return Ok(()); // No notification configured, silently succeed
182        }
183
184        // In a real implementation, this would send the actual notification
185        // For now, just validate that we have the configuration
186        let _config = self.get_task_notification(task_id).await?;
187
188        Ok(())
189    }
190
191    /// Send notification for task artifact update
192    async fn notify_task_artifact_update(
193        &self,
194        task_id: &str,
195        _artifact_update: &crate::domain::TaskArtifactUpdateEvent,
196    ) -> Result<(), A2AError> {
197        // Default implementation - can be overridden
198        // Check if notifications are configured for this task
199        if !self.has_task_notification(task_id).await? {
200            return Ok(()); // No notification configured, silently succeed
201        }
202
203        // In a real implementation, this would send the actual notification
204        // For now, just validate that we have the configuration
205        let _config = self.get_task_notification(task_id).await?;
206
207        Ok(())
208    }
209}