1use std::collections::HashMap;
8
9use serde_json::{Map, Value};
10
11use crate::types::str_list;
12
13pub const EVENTS: [&str; 4] = ["insert", "update", "remove", "schedule"];
17
18pub const OPS: [&str; 3] = ["insert", "update", "remove"];
20
21pub type Triggers = HashMap<String, Vec<TriggerDef>>;
23
24#[derive(Debug, Clone)]
25pub struct TriggerDef {
26 pub name: String,
28 pub when: Option<Value>,
30 pub on_fields: Vec<String>,
32 pub cron: Option<String>,
34 pub body: TriggerBody,
35}
36
37#[derive(Debug, Clone)]
38pub enum TriggerBody {
39 Command {
41 into: String,
42 op: String, condition: Option<Value>, data: Value, },
46 Callback { fn_ref: String, args: Value },
48}
49
50pub fn parse_triggers(
65 obj: &Map<String, Value>,
66 owner: &str,
67 fields: &HashMap<String, crate::schema::FieldDef>,
68) -> Result<Triggers, String> {
69 let mut out: Triggers = HashMap::new();
70 let Some(Value::Object(tm)) = obj.get("triggers") else {
71 return Ok(out);
72 };
73 for (event, val) in tm {
74 if !EVENTS.contains(&event.as_str()) {
75 return Err(format!(
76 "schema \"{owner}\" 的 triggers 事件键 \"{event}\" 非法(仅 insert/update/remove/schedule)"
77 ));
78 }
79 let arr = val
80 .as_array()
81 .ok_or_else(|| format!("schema \"{owner}\" 的 triggers.{event} 必须是数组"))?;
82 let mut list = Vec::new();
83 for (i, t) in arr.iter().enumerate() {
84 let to = t
85 .as_object()
86 .ok_or_else(|| format!("schema \"{owner}\" 的 triggers.{event}[{i}] 必须是对象"))?;
87 list.push(parse_one(owner, event, i, to, fields)?);
88 }
89 out.insert(event.clone(), list);
90 }
91 Ok(out)
92}
93
94fn parse_one(
95 owner: &str,
96 event: &str,
97 idx: usize,
98 to: &Map<String, Value>,
99 fields: &HashMap<String, crate::schema::FieldDef>,
100) -> Result<TriggerDef, String> {
101 let at = format!("{owner}.triggers.{event}[{idx}]");
102 let has_cmd = to.contains_key("into") || to.contains_key("op") || to.contains_key("data");
103 let has_cb = to.contains_key("fnRef");
104 if has_cmd == has_cb {
105 return Err(format!(
106 "{at} 必须二选一:命令式(into+op+data)或回调式(fnRef),不可同给或同缺"
107 ));
108 }
109 let on_fields = str_list(to.get("onFields")).unwrap_or_default();
111 for f in &on_fields {
112 if !f.starts_with('$') && !fields.contains_key(f) {
113 return Err(format!("{at} 的 onFields 字段 \"{f}\" 未在 schema 中声明"));
114 }
115 }
116 if let Some(w) = to.get("when") {
117 if !w.is_object() {
118 return Err(format!("{at} 的 when 必须是对象"));
119 }
120 }
121 if to.contains_key("cascade") {
122 return Err(format!("{at} 不支持 cascade(首批触发链只展开一层)"));
123 }
124 let cron = match to.get("cron") {
126 Some(Value::String(s)) => Some(s.clone()),
127 Some(_) => return Err(format!("{at} 的 cron 必须是字符串")),
128 None => None,
129 };
130 if event == "schedule" {
131 let c = cron
132 .as_deref()
133 .ok_or_else(|| format!("{at} 的 schedule 触发器缺少 cron"))?;
134 validate_cron(c).map_err(|e| format!("{at} 的 cron \"{c}\" 非法:{e}"))?;
135 forbid_root_before_refs(to.get("when"), &at)?;
137 if let Some(w) = to.get("args") {
138 forbid_root_before_refs(Some(w), &at)?;
139 }
140 if let Some(d) = to.get("data") {
141 forbid_root_before_refs(Some(d), &at)?;
142 }
143 if let Some(c) = to.get("condition") {
144 forbid_root_before_refs(Some(c), &at)?;
145 }
146 } else if cron.is_some() {
147 return Err(format!("{at} 的 cron 仅 schedule 事件可配"));
148 }
149 let name = to
150 .get("name")
151 .and_then(|v| v.as_str())
152 .filter(|s| !s.is_empty())
153 .unwrap_or("")
154 .to_string();
155
156 let body = if has_cb {
157 TriggerBody::Callback {
158 fn_ref: to
159 .get("fnRef")
160 .and_then(|v| v.as_str())
161 .filter(|s| !s.is_empty())
162 .ok_or_else(|| format!("{at} 的 fnRef 须为非空字符串"))?
163 .to_string(),
164 args: to.get("args").cloned().unwrap_or(Value::Null),
165 }
166 } else {
167 let into = to
168 .get("into")
169 .and_then(|v| v.as_str())
170 .filter(|s| !s.is_empty())
171 .ok_or_else(|| format!("{at} 缺少 into"))?
172 .to_string();
173 let op = to
174 .get("op")
175 .and_then(|v| v.as_str())
176 .ok_or_else(|| format!("{at} 缺少 op"))?
177 .to_string();
178 if !OPS.contains(&op.as_str()) {
179 return Err(format!(
180 "{at} 的 op \"{op}\" 不在白名单(insert/update/remove)"
181 ));
182 }
183 let data = match op.as_str() {
184 "remove" => {
186 if to.get("data").map(|d| !d.is_null()).unwrap_or(false) {
187 return Err(format!("{at} 的 op=remove 不接受 data"));
188 }
189 Value::Null
190 }
191 _ => to
192 .get("data")
193 .filter(|v| v.is_object())
194 .cloned()
195 .ok_or_else(|| format!("{at} 的命令式触发缺 data(须为对象)"))?,
196 };
197 let condition = to.get("condition").cloned();
198 if (op == "update" || op == "remove")
199 && !condition.as_ref().map(|v| v.is_object()).unwrap_or(false)
200 {
201 return Err(format!("{at} 的 op={op} 必须提供 condition(对象)"));
202 }
203 TriggerBody::Command {
204 into,
205 op,
206 condition,
207 data,
208 }
209 };
210 Ok(TriggerDef {
211 name,
212 when: to.get("when").cloned(),
213 on_fields,
214 cron,
215 body,
216 })
217}
218
219pub fn validate_cron(expr: &str) -> Result<(), String> {
221 let parts: Vec<&str> = expr.split_whitespace().collect();
222 if parts.len() != 5 {
223 return Err(format!(
224 "必须为 5 段(分 时 日 月 周),实为 {} 段",
225 parts.len()
226 ));
227 }
228 let ranges = [(0i64, 59), (0, 23), (1, 31), (1, 12), (0, 7)];
230 for (seg, (lo, hi)) in parts.iter().zip(ranges) {
231 if !seg
232 .chars()
233 .all(|c| c.is_ascii_digit() || matches!(c, '*' | ',' | '-' | '/'))
234 {
235 return Err(format!("段 \"{seg}\" 含非法字符(仅 * , - / 与数字)"));
236 }
237 for piece in seg.split(',') {
238 if piece.is_empty() {
239 return Err(format!("段 \"{seg}\" 存在空列表项"));
240 }
241 let body = match piece.split_once('/') {
242 Some((b, s)) => {
243 if s.is_empty() || !s.chars().all(|c| c.is_ascii_digit()) {
244 return Err(format!("段 \"{seg}\" 的步进 \"{s}\" 须为正整数"));
245 }
246 let n: i64 = s
247 .parse()
248 .map_err(|_| format!("段 \"{seg}\" 的步进 \"{s}\" 越界"))?;
249 if n <= 0 {
250 return Err(format!("段 \"{seg}\" 的步进 \"{s}\" 须为正整数"));
251 }
252 b
253 }
254 None => piece,
255 };
256 if body == "*" {
257 continue;
258 }
259 let (a, b) = match body.split_once('-') {
260 Some((x, y)) => (x, y),
261 None => (body, body),
262 };
263 let parse_bound = |s: &str| -> Result<i64, String> {
264 if s.is_empty() || !s.chars().all(|c| c.is_ascii_digit()) {
265 return Err(format!("段 \"{seg}\" 含非法形态 \"{piece}\""));
266 }
267 let n: i64 = s
268 .parse()
269 .map_err(|_| format!("段 \"{seg}\" 的 \"{s}\" 越界"))?;
270 if n < lo || n > hi {
271 return Err(format!("段 \"{seg}\" 的 {n} 超出范围 {lo}-{hi}"));
272 }
273 Ok(n)
274 };
275 let x = parse_bound(a)?;
276 let y = parse_bound(b)?;
277 if x > y {
278 return Err(format!("段 \"{seg}\" 区间 \"{piece}\" 起点大于终点"));
279 }
280 }
281 }
282 Ok(())
283}
284
285fn forbid_root_before_refs(v: Option<&Value>, at: &str) -> Result<(), String> {
287 match v {
288 Some(Value::String(s)) => {
289 if s.contains("{{root.") || s.contains("{{before.") {
290 return Err(format!(
291 "{at} 的 schedule 触发器不支持 {{{{root.}}}}/{{{{before.}}}} 占位符(仅 {{{{now}}}})"
292 ));
293 }
294 Ok(())
295 }
296 Some(Value::Array(a)) => a
297 .iter()
298 .try_for_each(|x| forbid_root_before_refs(Some(x), at)),
299 Some(Value::Object(o)) => o
300 .values()
301 .try_for_each(|x| forbid_root_before_refs(Some(x), at)),
302 _ => Ok(()),
303 }
304}