use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use uuid::Uuid;
use paladin_core::platform::container::schedule::Schedule;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum BridgeAction {
ScheduleJob,
QueueItem,
FireEvent,
SendNotification,
}
impl BridgeAction {
pub fn as_str(self) -> &'static str {
match self {
BridgeAction::ScheduleJob => "schedule_job",
BridgeAction::QueueItem => "queue_item",
BridgeAction::FireEvent => "fire_event",
BridgeAction::SendNotification => "send_notification",
}
}
}
#[derive(Debug, Clone)]
pub struct ScheduleJobRequest {
pub name: String,
pub description: String,
pub schedule: Schedule,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueueItemRequest {
pub queue_name: String,
pub payload: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FireEventRequest {
pub event_type: String,
pub payload: serde_json::Value,
pub source: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SendNotificationRequest {
pub channel: String,
pub recipient: String,
pub subject: String,
pub body: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct EventDispatchResult {
pub triggered_count: usize,
pub trigger_ids: Vec<Uuid>,
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum OrchestratorBridgeError {
#[error("Action not allowed by bridge policy: {0}")]
ActionNotAllowed(String),
#[error("Quota exceeded for action '{action}' (limit {limit})")]
QuotaExceeded {
action: String,
limit: u32,
},
#[error("Invalid request: {0}")]
InvalidRequest(String),
#[error("Orchestrator error: {0}")]
OrchestratorError(String),
}
#[derive(Debug, Clone)]
pub struct BridgePolicy {
allowed: HashSet<BridgeAction>,
max_jobs_scheduled: u32,
max_items_queued: u32,
max_events_fired: u32,
max_notifications_sent: u32,
}
impl BridgePolicy {
pub fn new(
allowed: HashSet<BridgeAction>,
max_jobs_scheduled: u32,
max_items_queued: u32,
max_events_fired: u32,
max_notifications_sent: u32,
) -> Self {
Self {
allowed,
max_jobs_scheduled,
max_items_queued,
max_events_fired,
max_notifications_sent,
}
}
pub fn is_allowed(&self, action: BridgeAction) -> bool {
self.allowed.contains(&action)
}
pub fn cap_for(&self, action: BridgeAction) -> u32 {
match action {
BridgeAction::ScheduleJob => self.max_jobs_scheduled,
BridgeAction::QueueItem => self.max_items_queued,
BridgeAction::FireEvent => self.max_events_fired,
BridgeAction::SendNotification => self.max_notifications_sent,
}
}
pub fn allow(mut self, action: BridgeAction) -> Self {
self.allowed.insert(action);
self
}
}
impl Default for BridgePolicy {
fn default() -> Self {
let mut allowed = HashSet::new();
allowed.insert(BridgeAction::ScheduleJob);
allowed.insert(BridgeAction::QueueItem);
allowed.insert(BridgeAction::FireEvent);
allowed.insert(BridgeAction::SendNotification);
Self {
allowed,
max_jobs_scheduled: 3,
max_items_queued: 3,
max_events_fired: 3,
max_notifications_sent: 3,
}
}
}
#[async_trait]
pub trait OrchestratorPort: Send + Sync {
async fn schedule_job(
&self,
request: ScheduleJobRequest,
) -> Result<Uuid, OrchestratorBridgeError>;
async fn queue_item(&self, request: QueueItemRequest) -> Result<Uuid, OrchestratorBridgeError>;
async fn fire_event(
&self,
request: FireEventRequest,
) -> Result<EventDispatchResult, OrchestratorBridgeError>;
async fn send_notification(
&self,
request: SendNotificationRequest,
) -> Result<Uuid, OrchestratorBridgeError>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_policy_allows_all_actions() {
let policy = BridgePolicy::default();
assert!(policy.is_allowed(BridgeAction::ScheduleJob));
assert!(policy.is_allowed(BridgeAction::QueueItem));
assert!(policy.is_allowed(BridgeAction::FireEvent));
assert!(policy.is_allowed(BridgeAction::SendNotification));
}
#[test]
fn default_policy_has_small_caps() {
let policy = BridgePolicy::default();
assert_eq!(policy.cap_for(BridgeAction::ScheduleJob), 3);
assert_eq!(policy.cap_for(BridgeAction::QueueItem), 3);
assert_eq!(policy.cap_for(BridgeAction::FireEvent), 3);
assert_eq!(policy.cap_for(BridgeAction::SendNotification), 3);
}
#[test]
fn empty_policy_disallows_actions() {
let policy = BridgePolicy::new(HashSet::new(), 0, 0, 0, 0);
assert!(!policy.is_allowed(BridgeAction::ScheduleJob));
assert!(!policy.is_allowed(BridgeAction::FireEvent));
}
#[test]
fn allow_builder_adds_action() {
let policy = BridgePolicy::new(HashSet::new(), 1, 1, 1, 1).allow(BridgeAction::ScheduleJob);
assert!(policy.is_allowed(BridgeAction::ScheduleJob));
assert!(!policy.is_allowed(BridgeAction::QueueItem));
}
#[test]
fn bridge_action_as_str_matches() {
assert_eq!(BridgeAction::ScheduleJob.as_str(), "schedule_job");
assert_eq!(BridgeAction::QueueItem.as_str(), "queue_item");
assert_eq!(BridgeAction::FireEvent.as_str(), "fire_event");
assert_eq!(BridgeAction::SendNotification.as_str(), "send_notification");
}
#[test]
fn custom_caps_are_reported() {
let mut allowed = HashSet::new();
allowed.insert(BridgeAction::ScheduleJob);
let policy = BridgePolicy::new(allowed, 5, 7, 9, 11);
assert_eq!(policy.cap_for(BridgeAction::ScheduleJob), 5);
assert_eq!(policy.cap_for(BridgeAction::QueueItem), 7);
assert_eq!(policy.cap_for(BridgeAction::FireEvent), 9);
assert_eq!(policy.cap_for(BridgeAction::SendNotification), 11);
}
}