Skip to main content

rust_store_core/schema/
triggers.rs

1//! Schema 触发器声明:解析与校验(规划见 `crate::command::triggers`)。
2//!
3//! 声明形态(schema JSON 顶层 `triggers` 键):
4//!   "triggers": { "insert": [T...], "update": [T...] }
5//! T 字段契约见执行总纲「常驻契约卡」。本模块只做**结构校验**,不做运行时判定。
6
7use std::collections::HashMap;
8
9use serde_json::{Map, Value};
10
11use crate::types::str_list;
12
13/// 事件名(首批两类:insert / update;其余键注册期 Err —— remove 触发语义不明,暂不支持)
14pub const EVENTS: [&str; 2] = ["insert", "update"];
15
16/// 命令式触发支持的 op 白名单(首批不含 upsert)
17pub const OPS: [&str; 2] = ["insert", "update"];
18
19/// `triggers` = 事件名 → 触发列表
20pub type Triggers = HashMap<String, Vec<TriggerDef>>;
21
22#[derive(Debug, Clone)]
23pub struct TriggerDef {
24    /// 显式名(空 = 缺省 `<Schema>.<event>[<i>]`)
25    pub name: String,
26    /// 运行时条件(占位符替换后判定);`None` = 恒真
27    pub when: Option<Value>,
28    /// 字段级:仅这些字段「值真的变化」才进入判定;空 = 记录级
29    pub on_fields: Vec<String>,
30    pub body: TriggerBody,
31}
32
33#[derive(Debug, Clone)]
34pub enum TriggerBody {
35    /// 命令式:写目标 schema
36    Command {
37        into: String,
38        op: String,               // ∈ OPS
39        condition: Option<Value>, // op=update 必填
40        data: Value,              // 必填
41    },
42    /// 回调式:Host 侧 fnRef 实现
43    Callback { fn_ref: String, args: Value },
44}
45
46/// 解析并校验 schema 顶层的 `triggers`;任一项非法 → Err(零静默)。
47///
48/// 校验清单(全部命中即 Err):
49///  1. `triggers` 非对象;
50///  2. 事件键不在 `EVENTS`;
51///  3. 事件值非数组 / 元素非对象;
52///  4. `into`/`op`/`data`(命令式)与 `fnRef`(回调式)**互斥且必居其一**;
53///  5. `op` 不在 `OPS`;
54///  6. 命令式缺 `data`,或 `op=update` 缺 `condition`;
55///  7. `onFields` 非字符串数组,或字段未在 `fields` 中声明(`$` 前缀键除外);
56///  8. `when` 非对象;出现 `cascade` 键(首批不支持级联);
57///  9. 命令式 `data` 的键未在**目标 schema** 中声明(在展开期校验,见 `command::triggers`)。
58pub fn parse_triggers(
59    obj: &Map<String, Value>,
60    owner: &str,
61    fields: &HashMap<String, crate::schema::FieldDef>,
62) -> Result<Triggers, String> {
63    let mut out: Triggers = HashMap::new();
64    let Some(Value::Object(tm)) = obj.get("triggers") else {
65        return Ok(out);
66    };
67    for (event, val) in tm {
68        if !EVENTS.contains(&event.as_str()) {
69            return Err(format!(
70                "schema \"{owner}\" 的 triggers 事件键 \"{event}\" 非法(仅 insert/update)"
71            ));
72        }
73        let arr = val
74            .as_array()
75            .ok_or_else(|| format!("schema \"{owner}\" 的 triggers.{event} 必须是数组"))?;
76        let mut list = Vec::new();
77        for (i, t) in arr.iter().enumerate() {
78            let to = t
79                .as_object()
80                .ok_or_else(|| format!("schema \"{owner}\" 的 triggers.{event}[{i}] 必须是对象"))?;
81            list.push(parse_one(owner, event, i, to, fields)?);
82        }
83        out.insert(event.clone(), list);
84    }
85    Ok(out)
86}
87
88fn parse_one(
89    owner: &str,
90    event: &str,
91    idx: usize,
92    to: &Map<String, Value>,
93    fields: &HashMap<String, crate::schema::FieldDef>,
94) -> Result<TriggerDef, String> {
95    let at = format!("{owner}.triggers.{event}[{idx}]");
96    let has_cmd = to.contains_key("into") || to.contains_key("op") || to.contains_key("data");
97    let has_cb = to.contains_key("fnRef");
98    if has_cmd == has_cb {
99        return Err(format!(
100            "{at} 必须二选一:命令式(into+op+data)或回调式(fnRef),不可同给或同缺"
101        ));
102    }
103    // onFields 字段声明校验(仅 update 有意义;insert/remove 声明了也接受但忽略)
104    let on_fields = str_list(to.get("onFields")).unwrap_or_default();
105    for f in &on_fields {
106        if !f.starts_with('$') && !fields.contains_key(f) {
107            return Err(format!("{at} 的 onFields 字段 \"{f}\" 未在 schema 中声明"));
108        }
109    }
110    if let Some(w) = to.get("when") {
111        if !w.is_object() {
112            return Err(format!("{at} 的 when 必须是对象"));
113        }
114    }
115    if to.contains_key("cascade") {
116        return Err(format!("{at} 不支持 cascade(首批触发链只展开一层)"));
117    }
118    let name = to
119        .get("name")
120        .and_then(|v| v.as_str())
121        .filter(|s| !s.is_empty())
122        .unwrap_or("")
123        .to_string();
124
125    let body = if has_cb {
126        TriggerBody::Callback {
127            fn_ref: to
128                .get("fnRef")
129                .and_then(|v| v.as_str())
130                .filter(|s| !s.is_empty())
131                .ok_or_else(|| format!("{at} 的 fnRef 须为非空字符串"))?
132                .to_string(),
133            args: to.get("args").cloned().unwrap_or(Value::Null),
134        }
135    } else {
136        let into = to
137            .get("into")
138            .and_then(|v| v.as_str())
139            .filter(|s| !s.is_empty())
140            .ok_or_else(|| format!("{at} 缺少 into"))?
141            .to_string();
142        let op = to
143            .get("op")
144            .and_then(|v| v.as_str())
145            .ok_or_else(|| format!("{at} 缺少 op"))?
146            .to_string();
147        if !OPS.contains(&op.as_str()) {
148            return Err(format!("{at} 的 op \"{op}\" 不在白名单(insert/update)"));
149        }
150        let data = to
151            .get("data")
152            .filter(|v| v.is_object())
153            .cloned()
154            .ok_or_else(|| format!("{at} 的命令式触发缺 data(须为对象)"))?;
155        let condition = to.get("condition").cloned();
156        if op == "update" && !condition.as_ref().map(|v| v.is_object()).unwrap_or(false) {
157            return Err(format!("{at} 的 op=update 必须提供 condition(对象)"));
158        }
159        TriggerBody::Command {
160            into,
161            op,
162            condition,
163            data,
164        }
165    };
166    Ok(TriggerDef {
167        name,
168        when: to.get("when").cloned(),
169        on_fields,
170        body,
171    })
172}