use std::collections::HashMap;
use serde_json::{Map, Value};
use crate::types::str_list;
pub const EVENTS: [&str; 4] = ["insert", "update", "remove", "schedule"];
pub const OPS: [&str; 3] = ["insert", "update", "remove"];
pub type Triggers = HashMap<String, Vec<TriggerDef>>;
#[derive(Debug, Clone)]
pub struct TriggerDef {
pub name: String,
pub when: Option<Value>,
pub on_fields: Vec<String>,
pub cron: Option<String>,
pub body: TriggerBody,
}
#[derive(Debug, Clone)]
pub enum TriggerBody {
Command {
into: String,
op: String, condition: Option<Value>, data: Value, },
Callback { fn_ref: String, args: Value },
}
pub fn parse_triggers(
obj: &Map<String, Value>,
owner: &str,
fields: &HashMap<String, crate::schema::FieldDef>,
) -> Result<Triggers, String> {
let mut out: Triggers = HashMap::new();
let Some(Value::Object(tm)) = obj.get("triggers") else {
return Ok(out);
};
for (event, val) in tm {
if !EVENTS.contains(&event.as_str()) {
return Err(format!(
"schema \"{owner}\" 的 triggers 事件键 \"{event}\" 非法(仅 insert/update/remove/schedule)"
));
}
let arr = val
.as_array()
.ok_or_else(|| format!("schema \"{owner}\" 的 triggers.{event} 必须是数组"))?;
let mut list = Vec::new();
for (i, t) in arr.iter().enumerate() {
let to = t
.as_object()
.ok_or_else(|| format!("schema \"{owner}\" 的 triggers.{event}[{i}] 必须是对象"))?;
list.push(parse_one(owner, event, i, to, fields)?);
}
out.insert(event.clone(), list);
}
Ok(out)
}
fn parse_one(
owner: &str,
event: &str,
idx: usize,
to: &Map<String, Value>,
fields: &HashMap<String, crate::schema::FieldDef>,
) -> Result<TriggerDef, String> {
let at = format!("{owner}.triggers.{event}[{idx}]");
let has_cmd = to.contains_key("into") || to.contains_key("op") || to.contains_key("data");
let has_cb = to.contains_key("fnRef");
if has_cmd == has_cb {
return Err(format!(
"{at} 必须二选一:命令式(into+op+data)或回调式(fnRef),不可同给或同缺"
));
}
let on_fields = str_list(to.get("onFields")).unwrap_or_default();
for f in &on_fields {
if !f.starts_with('$') && !fields.contains_key(f) {
return Err(format!("{at} 的 onFields 字段 \"{f}\" 未在 schema 中声明"));
}
}
if let Some(w) = to.get("when") {
if !w.is_object() {
return Err(format!("{at} 的 when 必须是对象"));
}
}
if to.contains_key("cascade") {
return Err(format!("{at} 不支持 cascade(首批触发链只展开一层)"));
}
let cron = match to.get("cron") {
Some(Value::String(s)) => Some(s.clone()),
Some(_) => return Err(format!("{at} 的 cron 必须是字符串")),
None => None,
};
if event == "schedule" {
let c = cron
.as_deref()
.ok_or_else(|| format!("{at} 的 schedule 触发器缺少 cron"))?;
validate_cron(c).map_err(|e| format!("{at} 的 cron \"{c}\" 非法:{e}"))?;
forbid_root_before_refs(to.get("when"), &at)?;
if let Some(w) = to.get("args") {
forbid_root_before_refs(Some(w), &at)?;
}
if let Some(d) = to.get("data") {
forbid_root_before_refs(Some(d), &at)?;
}
if let Some(c) = to.get("condition") {
forbid_root_before_refs(Some(c), &at)?;
}
} else if cron.is_some() {
return Err(format!("{at} 的 cron 仅 schedule 事件可配"));
}
let name = to
.get("name")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.unwrap_or("")
.to_string();
let body = if has_cb {
TriggerBody::Callback {
fn_ref: to
.get("fnRef")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.ok_or_else(|| format!("{at} 的 fnRef 须为非空字符串"))?
.to_string(),
args: to.get("args").cloned().unwrap_or(Value::Null),
}
} else {
let into = to
.get("into")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.ok_or_else(|| format!("{at} 缺少 into"))?
.to_string();
let op = to
.get("op")
.and_then(|v| v.as_str())
.ok_or_else(|| format!("{at} 缺少 op"))?
.to_string();
if !OPS.contains(&op.as_str()) {
return Err(format!(
"{at} 的 op \"{op}\" 不在白名单(insert/update/remove)"
));
}
let data = match op.as_str() {
"remove" => {
if to.get("data").map(|d| !d.is_null()).unwrap_or(false) {
return Err(format!("{at} 的 op=remove 不接受 data"));
}
Value::Null
}
_ => to
.get("data")
.filter(|v| v.is_object())
.cloned()
.ok_or_else(|| format!("{at} 的命令式触发缺 data(须为对象)"))?,
};
let condition = to.get("condition").cloned();
if (op == "update" || op == "remove")
&& !condition.as_ref().map(|v| v.is_object()).unwrap_or(false)
{
return Err(format!("{at} 的 op={op} 必须提供 condition(对象)"));
}
TriggerBody::Command {
into,
op,
condition,
data,
}
};
Ok(TriggerDef {
name,
when: to.get("when").cloned(),
on_fields,
cron,
body,
})
}
pub fn validate_cron(expr: &str) -> Result<(), String> {
let parts: Vec<&str> = expr.split_whitespace().collect();
if parts.len() != 5 {
return Err(format!(
"必须为 5 段(分 时 日 月 周),实为 {} 段",
parts.len()
));
}
let ranges = [(0i64, 59), (0, 23), (1, 31), (1, 12), (0, 7)];
for (seg, (lo, hi)) in parts.iter().zip(ranges) {
if !seg
.chars()
.all(|c| c.is_ascii_digit() || matches!(c, '*' | ',' | '-' | '/'))
{
return Err(format!("段 \"{seg}\" 含非法字符(仅 * , - / 与数字)"));
}
for piece in seg.split(',') {
if piece.is_empty() {
return Err(format!("段 \"{seg}\" 存在空列表项"));
}
let body = match piece.split_once('/') {
Some((b, s)) => {
if s.is_empty() || !s.chars().all(|c| c.is_ascii_digit()) {
return Err(format!("段 \"{seg}\" 的步进 \"{s}\" 须为正整数"));
}
let n: i64 = s
.parse()
.map_err(|_| format!("段 \"{seg}\" 的步进 \"{s}\" 越界"))?;
if n <= 0 {
return Err(format!("段 \"{seg}\" 的步进 \"{s}\" 须为正整数"));
}
b
}
None => piece,
};
if body == "*" {
continue;
}
let (a, b) = match body.split_once('-') {
Some((x, y)) => (x, y),
None => (body, body),
};
let parse_bound = |s: &str| -> Result<i64, String> {
if s.is_empty() || !s.chars().all(|c| c.is_ascii_digit()) {
return Err(format!("段 \"{seg}\" 含非法形态 \"{piece}\""));
}
let n: i64 = s
.parse()
.map_err(|_| format!("段 \"{seg}\" 的 \"{s}\" 越界"))?;
if n < lo || n > hi {
return Err(format!("段 \"{seg}\" 的 {n} 超出范围 {lo}-{hi}"));
}
Ok(n)
};
let x = parse_bound(a)?;
let y = parse_bound(b)?;
if x > y {
return Err(format!("段 \"{seg}\" 区间 \"{piece}\" 起点大于终点"));
}
}
}
Ok(())
}
fn forbid_root_before_refs(v: Option<&Value>, at: &str) -> Result<(), String> {
match v {
Some(Value::String(s)) => {
if s.contains("{{root.") || s.contains("{{before.") {
return Err(format!(
"{at} 的 schedule 触发器不支持 {{{{root.}}}}/{{{{before.}}}} 占位符(仅 {{{{now}}}})"
));
}
Ok(())
}
Some(Value::Array(a)) => a
.iter()
.try_for_each(|x| forbid_root_before_refs(Some(x), at)),
Some(Value::Object(o)) => o
.values()
.try_for_each(|x| forbid_root_before_refs(Some(x), at)),
_ => Ok(()),
}
}