use serde::{Deserialize, Serialize};
use crate::{
FunctionDefinition,
triggers::{
http::{HttpTriggerMatcher, HttpTriggerRoute},
mutation::{AfterMutationTrigger, BeforeMutationTrigger, TriggerMatcher},
},
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegistryError {
pub message: String,
}
impl std::fmt::Display for RegistryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for RegistryError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParsedTrigger {
AfterMutation {
entity_type: String,
operation: Option<String>,
},
BeforeMutation {
mutation_name: String,
},
AfterStorage {
bucket: String,
operation: String,
},
Cron {
expression: String,
},
Http {
method: String,
path: String,
},
}
impl ParsedTrigger {
pub fn parse(trigger: &str) -> Result<Self, RegistryError> {
let parts: Vec<&str> = trigger.split(':').collect();
match parts.first().copied() {
Some("after") if parts.len() >= 3 && parts[1] == "mutation" => {
let entity_type = parts[2].to_string();
let operation = if parts.len() > 3 {
Some(parts[3].to_string())
} else {
None
};
Ok(ParsedTrigger::AfterMutation {
entity_type,
operation,
})
},
Some("before") if parts.len() >= 3 && parts[1] == "mutation" => {
let mutation_name = parts[2].to_string();
Ok(ParsedTrigger::BeforeMutation { mutation_name })
},
Some("after") if parts.len() >= 4 && parts[1] == "storage" => {
let bucket = parts[2].to_string();
let operation = parts[3].to_string();
Ok(ParsedTrigger::AfterStorage { bucket, operation })
},
Some("cron") if parts.len() >= 2 => {
let expression = parts[1..].join(":");
Ok(ParsedTrigger::Cron { expression })
},
Some("http") if parts.len() >= 3 => {
let method = parts[1].to_string();
let path = parts[2..].join(":");
Ok(ParsedTrigger::Http { method, path })
},
_ => Err(RegistryError {
message: format!("Invalid trigger format: {}", trigger),
}),
}
}
#[must_use]
pub const fn trigger_type(&self) -> &'static str {
match self {
ParsedTrigger::AfterMutation { .. } => "after:mutation",
ParsedTrigger::BeforeMutation { .. } => "before:mutation",
ParsedTrigger::AfterStorage { .. } => "after:storage",
ParsedTrigger::Cron { .. } => "cron",
ParsedTrigger::Http { .. } => "http",
}
}
#[must_use]
pub const fn is_after_mutation(&self) -> bool {
matches!(self, ParsedTrigger::AfterMutation { .. })
}
#[must_use]
pub const fn is_before_mutation(&self) -> bool {
matches!(self, ParsedTrigger::BeforeMutation { .. })
}
#[must_use]
pub const fn is_http(&self) -> bool {
matches!(self, ParsedTrigger::Http { .. })
}
#[must_use]
pub const fn is_cron(&self) -> bool {
matches!(self, ParsedTrigger::Cron { .. })
}
#[must_use]
pub const fn is_after_storage(&self) -> bool {
matches!(self, ParsedTrigger::AfterStorage { .. })
}
}
#[derive(Debug, Default)]
pub struct TriggerRegistry {
pub after_mutation_triggers: TriggerMatcher,
pub before_mutation_triggers: Vec<BeforeMutationTrigger>,
pub http_routes: HttpTriggerMatcher,
pub cron_triggers: Vec<crate::triggers::cron::CronTrigger>,
pub function_count: usize,
}
impl TriggerRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn load_from_definitions(functions: &[FunctionDefinition]) -> Result<Self, RegistryError> {
let mut registry = Self::new();
registry.function_count = functions.len();
for func in functions {
let parsed = ParsedTrigger::parse(&func.trigger)?;
match parsed {
ParsedTrigger::AfterMutation {
entity_type,
operation,
} => {
let trigger = AfterMutationTrigger {
function_name: func.name.clone(),
entity_type,
event_filter: operation.as_ref().and_then(|op| match op.as_str() {
"insert" => Some(crate::EventKind::Insert),
"update" => Some(crate::EventKind::Update),
"delete" => Some(crate::EventKind::Delete),
_ => None,
}),
};
registry.after_mutation_triggers.add(trigger);
},
ParsedTrigger::BeforeMutation { mutation_name } => {
let trigger = BeforeMutationTrigger {
function_name: func.name.clone(),
mutation_name,
};
registry.before_mutation_triggers.push(trigger);
},
ParsedTrigger::Http { method, path } => {
let route = HttpTriggerRoute {
function_name: func.name.clone(),
method,
path,
requires_auth: false,
};
registry.http_routes.add(route);
},
ParsedTrigger::AfterStorage {
bucket: _,
operation: _,
} => {
return Err(RegistryError {
message: "after:storage triggers not yet implemented".to_string(),
});
},
ParsedTrigger::Cron { expression } => {
let trigger = crate::triggers::cron::CronTrigger {
function_name: func.name.clone(),
schedule: expression,
timezone: "UTC".to_string(),
};
registry.cron_triggers.push(trigger);
},
}
}
Ok(registry)
}
#[must_use]
pub const fn after_mutation_count(&self) -> usize {
0
}
#[must_use]
pub const fn before_mutation_count(&self) -> usize {
self.before_mutation_triggers.len()
}
#[must_use]
pub const fn cron_trigger_count(&self) -> usize {
self.cron_triggers.len()
}
#[must_use]
pub fn cron_scheduler(&self) -> Option<crate::triggers::cron::CronScheduler> {
if self.cron_triggers.is_empty() {
None
} else {
Some(crate::triggers::cron::CronScheduler::new(self.cron_triggers.clone()))
}
}
#[must_use]
pub fn http_route_count(&self) -> usize {
self.http_routes.routes().len()
}
#[must_use]
pub fn http_routes(&self) -> &[HttpTriggerRoute] {
self.http_routes.routes()
}
#[must_use]
pub fn find_http_route(&self, method: &str, path: &str) -> Option<HttpTriggerRoute> {
self.http_routes.find(method, path)
}
#[must_use]
pub fn before_mutation_triggers_for(&self, mutation_name: &str) -> Vec<&BeforeMutationTrigger> {
self.before_mutation_triggers
.iter()
.filter(|t| t.mutation_name == mutation_name)
.collect()
}
#[must_use]
pub fn has_before_mutation_triggers(&self, mutation_name: &str) -> bool {
self.before_mutation_triggers.iter().any(|t| t.mutation_name == mutation_name)
}
#[must_use]
pub fn before_chain(
&self,
mutation_name: &str,
) -> Option<crate::triggers::mutation::BeforeMutationChain> {
let triggers: Vec<_> = self
.before_mutation_triggers
.iter()
.filter(|t| t.mutation_name == mutation_name)
.cloned()
.collect();
if triggers.is_empty() {
None
} else {
Some(crate::triggers::mutation::BeforeMutationChain { triggers })
}
}
}
#[cfg(test)]
mod tests;