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!("{at} 的 op \"{op}\" 不在白名单(insert/update/remove)"));
180 }
181 let data = match op.as_str() {
182 "remove" => {
184 if to.get("data").map(|d| !d.is_null()).unwrap_or(false) {
185 return Err(format!("{at} 的 op=remove 不接受 data"));
186 }
187 Value::Null
188 }
189 _ => to
190 .get("data")
191 .filter(|v| v.is_object())
192 .cloned()
193 .ok_or_else(|| format!("{at} 的命令式触发缺 data(须为对象)"))?,
194 };
195 let condition = to.get("condition").cloned();
196 if (op == "update" || op == "remove")
197 && !condition.as_ref().map(|v| v.is_object()).unwrap_or(false)
198 {
199 return Err(format!("{at} 的 op={op} 必须提供 condition(对象)"));
200 }
201 TriggerBody::Command {
202 into,
203 op,
204 condition,
205 data,
206 }
207 };
208 Ok(TriggerDef {
209 name,
210 when: to.get("when").cloned(),
211 on_fields,
212 cron,
213 body,
214 })
215}
216
217pub fn validate_cron(expr: &str) -> Result<(), String> {
219 let parts: Vec<&str> = expr.split_whitespace().collect();
220 if parts.len() != 5 {
221 return Err(format!(
222 "必须为 5 段(分 时 日 月 周),实为 {} 段",
223 parts.len()
224 ));
225 }
226 let ranges = [(0i64, 59), (0, 23), (1, 31), (1, 12), (0, 7)];
228 for (seg, (lo, hi)) in parts.iter().zip(ranges) {
229 if !seg
230 .chars()
231 .all(|c| c.is_ascii_digit() || matches!(c, '*' | ',' | '-' | '/'))
232 {
233 return Err(format!("段 \"{seg}\" 含非法字符(仅 * , - / 与数字)"));
234 }
235 for piece in seg.split(',') {
236 if piece.is_empty() {
237 return Err(format!("段 \"{seg}\" 存在空列表项"));
238 }
239 let body = match piece.split_once('/') {
240 Some((b, s)) => {
241 if s.is_empty() || !s.chars().all(|c| c.is_ascii_digit()) {
242 return Err(format!("段 \"{seg}\" 的步进 \"{s}\" 须为正整数"));
243 }
244 let n: i64 = s
245 .parse()
246 .map_err(|_| format!("段 \"{seg}\" 的步进 \"{s}\" 越界"))?;
247 if n <= 0 {
248 return Err(format!("段 \"{seg}\" 的步进 \"{s}\" 须为正整数"));
249 }
250 b
251 }
252 None => piece,
253 };
254 if body == "*" {
255 continue;
256 }
257 let (a, b) = match body.split_once('-') {
258 Some((x, y)) => (x, y),
259 None => (body, body),
260 };
261 let parse_bound = |s: &str| -> Result<i64, String> {
262 if s.is_empty() || !s.chars().all(|c| c.is_ascii_digit()) {
263 return Err(format!("段 \"{seg}\" 含非法形态 \"{piece}\""));
264 }
265 let n: i64 = s
266 .parse()
267 .map_err(|_| format!("段 \"{seg}\" 的 \"{s}\" 越界"))?;
268 if n < lo || n > hi {
269 return Err(format!("段 \"{seg}\" 的 {n} 超出范围 {lo}-{hi}"));
270 }
271 Ok(n)
272 };
273 let x = parse_bound(a)?;
274 let y = parse_bound(b)?;
275 if x > y {
276 return Err(format!("段 \"{seg}\" 区间 \"{piece}\" 起点大于终点"));
277 }
278 }
279 }
280 Ok(())
281}
282
283fn forbid_root_before_refs(v: Option<&Value>, at: &str) -> Result<(), String> {
285 match v {
286 Some(Value::String(s)) => {
287 if s.contains("{{root.") || s.contains("{{before.") {
288 return Err(format!(
289 "{at} 的 schedule 触发器不支持 {{{{root.}}}}/{{{{before.}}}} 占位符(仅 {{{{now}}}})"
290 ));
291 }
292 Ok(())
293 }
294 Some(Value::Array(a)) => a
295 .iter()
296 .try_for_each(|x| forbid_root_before_refs(Some(x), at)),
297 Some(Value::Object(o)) => o
298 .values()
299 .try_for_each(|x| forbid_root_before_refs(Some(x), at)),
300 _ => Ok(()),
301 }
302}