use async_trait::async_trait;
use crate::domain::{
A2AError, DeleteTaskPushNotificationConfigParams, GetTaskPushNotificationConfigParams,
ListTaskPushNotificationConfigsParams, TaskArtifactUpdateEvent, TaskPushNotificationConfig,
TaskStatusUpdateEvent,
};
fn validate_push_notification_url(config: &TaskPushNotificationConfig) -> Result<(), A2AError> {
if config.url.trim().is_empty() {
return Err(A2AError::ValidationError {
field: "url".to_string(),
message: "Webhook URL cannot be empty".to_string(),
});
}
match url::Url::parse(&config.url) {
Ok(parsed_url) => {
let scheme = parsed_url.scheme();
if scheme != "https" {
let is_localhost = parsed_url
.host_str()
.map(|h| h == "localhost" || h == "127.0.0.1" || h == "::1")
.unwrap_or(false);
if scheme != "http" || !is_localhost {
return Err(A2AError::ValidationError {
field: "url".to_string(),
message: "Webhook URL must use HTTPS (HTTP is only allowed for localhost)"
.to_string(),
});
}
}
}
Err(_) => {
return Err(A2AError::ValidationError {
field: "url".to_string(),
message: "Invalid webhook URL format".to_string(),
});
}
}
Ok(())
}
#[async_trait]
pub trait AsyncNotificationManager: Send + Sync {
async fn set_config(
&self,
config: &TaskPushNotificationConfig,
) -> Result<TaskPushNotificationConfig, A2AError>;
async fn get_config(
&self,
params: &GetTaskPushNotificationConfigParams,
) -> Result<TaskPushNotificationConfig, A2AError>;
async fn list_configs(
&self,
params: &ListTaskPushNotificationConfigsParams,
) -> Result<Vec<TaskPushNotificationConfig>, A2AError>;
async fn delete_config(
&self,
params: &DeleteTaskPushNotificationConfigParams,
) -> Result<(), A2AError>;
}
#[async_trait]
pub trait AsyncNotificationManagerExt: AsyncNotificationManager {
fn validate_config(&self, config: &TaskPushNotificationConfig) -> Result<(), A2AError> {
validate_push_notification_url(config)
}
async fn set_validated(
&self,
config: &TaskPushNotificationConfig,
) -> Result<TaskPushNotificationConfig, A2AError> {
if config.task_id.trim().is_empty() {
return Err(A2AError::ValidationError {
field: "task_id".to_string(),
message: "Task ID cannot be empty".to_string(),
});
}
self.validate_config(config)?;
self.set_config(config).await
}
}
impl<T: AsyncNotificationManager + ?Sized> AsyncNotificationManagerExt for T {}
#[async_trait]
pub trait AsyncPushNotifier: Send + Sync {
async fn notify_status(
&self,
task_id: &str,
event: &TaskStatusUpdateEvent,
) -> Result<(), A2AError>;
async fn notify_artifact(
&self,
task_id: &str,
event: &TaskArtifactUpdateEvent,
) -> Result<(), A2AError>;
}
#[async_trait]
impl<T: AsyncPushNotifier + ?Sized> AsyncPushNotifier for std::sync::Arc<T> {
async fn notify_status(
&self,
task_id: &str,
event: &TaskStatusUpdateEvent,
) -> Result<(), A2AError> {
(**self).notify_status(task_id, event).await
}
async fn notify_artifact(
&self,
task_id: &str,
event: &TaskArtifactUpdateEvent,
) -> Result<(), A2AError> {
(**self).notify_artifact(task_id, event).await
}
}
#[derive(Clone, Debug, Default)]
pub struct NoopPushNotifier;
#[async_trait]
impl AsyncPushNotifier for NoopPushNotifier {
async fn notify_status(
&self,
_task_id: &str,
_event: &TaskStatusUpdateEvent,
) -> Result<(), A2AError> {
Ok(())
}
async fn notify_artifact(
&self,
_task_id: &str,
_event: &TaskArtifactUpdateEvent,
) -> Result<(), A2AError> {
Ok(())
}
}