use std::collections::HashMap;
use serde_json::{Map, Value};
use crate::types::str_list;
pub const EVENTS: [&str; 2] = ["insert", "update"];
pub const OPS: [&str; 2] = ["insert", "update"];
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 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)"
));
}
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 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)"));
}
let data = to
.get("data")
.filter(|v| v.is_object())
.cloned()
.ok_or_else(|| format!("{at} 的命令式触发缺 data(须为对象)"))?;
let condition = to.get("condition").cloned();
if op == "update" && !condition.as_ref().map(|v| v.is_object()).unwrap_or(false) {
return Err(format!("{at} 的 op=update 必须提供 condition(对象)"));
}
TriggerBody::Command {
into,
op,
condition,
data,
}
};
Ok(TriggerDef {
name,
when: to.get("when").cloned(),
on_fields,
body,
})
}