Skip to main content

zerolaunch_plugin_api/config/
setting_def.rs

1use crate::config::{DetailActionDef, FieldAction};
2use once_cell::sync::Lazy;
3use regex::Regex;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
7use std::sync::Mutex;
8
9/// 编译后的正则表达式缓存,按 pattern 字符串共享。
10/// validate_node 中每次匹配都从该缓存取编译结果,避免重复编译。
11static REGEX_CACHE: Lazy<Mutex<HashMap<String, Regex>>> = Lazy::new(|| Mutex::new(HashMap::new()));
12/// Settings schema 版本号,用于向前兼容判断。
13pub const SETTINGS_SCHEMA_VERSION: u32 = 1;
14
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
16pub enum CommitPolicy {
17    /// 用户必须点击"保存"按钮才提交。
18    #[default]
19    #[serde(rename = "staged")]
20    Staged,
21    /// 值变更时立即提交,跳过暂存。
22    #[serde(rename = "immediateAllowed")]
23    ImmediateAllowed,
24}
25
26/// 路径选择模式。
27#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
28pub enum PathMode {
29    /// 文件选择。
30    #[serde(rename = "file")]
31    File,
32    /// 目录选择。
33    #[serde(rename = "directory")]
34    Directory,
35}
36
37/// UI 控件提示 — 告诉前端这个字段应该用什么控件渲染。
38///
39/// 与 `SchemaKind` 正交:SchemaKind 描述数据形状,WidgetHint 描述呈现方式。
40/// 同一份数据(如 string)可以透过多样的 WidgetHint 渲染为不同控件。
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
42#[serde(tag = "kind")]
43pub enum WidgetHint {
44    /// 单行文本输入框。
45    #[serde(rename = "text")]
46    Text,
47    /// 多行文本域。
48    #[serde(rename = "textarea")]
49    Textarea,
50    /// 数字输入框(步进器)。
51    #[serde(rename = "number")]
52    Number,
53    /// 切换开关(用于 boolean 字段)。
54    #[serde(rename = "toggle")]
55    Toggle,
56    /// 下拉选择器。
57    #[serde(rename = "select")]
58    Select,
59    /// 多选复选框组(固定选项多选,值为字符串数组)。
60    #[serde(rename = "multiselect")]
61    MultiSelect,
62    /// 路径选择器(文件/目录)。
63    #[serde(rename = "path")]
64    Path {
65        /// 选择模式:文件或目录。
66        #[serde(rename = "mode")]
67        mode: PathMode,
68    },
69    /// 颜色选择器。
70    #[serde(rename = "color")]
71    Color,
72    /// 图片选择器。
73    #[serde(rename = "image")]
74    Image {
75        /// 允许的文件扩展名列表。
76        #[serde(rename = "accept")]
77        accept: Vec<String>,
78        /// 最大文件大小(字节)。
79        #[serde(rename = "maxSize")]
80        #[serde(default)]
81        max_size: Option<u64>,
82    },
83    /// 字体选择器 — 通过组件 config action 列出系统字体供用户直接选择。
84    #[serde(rename = "font")]
85    Font {
86        /// 列出系统字体的 config action 名称(如 `list_fonts`)。
87        #[serde(rename = "action")]
88        action: String,
89        /// 提供该 action 的组件 id;None 表示字段所属组件自身。
90        #[serde(rename = "component", default)]
91        component: Option<String>,
92    },
93    /// 快捷键录制器 — 聚焦后按下组合键进行录制,值格式为修饰键 + 主键(如 "Alt+Space")。
94    #[serde(rename = "hotkey")]
95    Hotkey {
96        /// 是否强制包含至少一个修饰键。全局快捷键为 true:
97        /// 无修饰键的按键注册为系统级热键会劫持全系统该键输入。
98        #[serde(rename = "requireModifier", default)]
99        require_modifier: bool,
100    },
101    /// 普通列表编辑器(默认的数组 UI)。
102    #[serde(rename = "list")]
103    List,
104    /// 标签式编辑器。
105    #[serde(rename = "tags")]
106    Tags,
107    /// 表格编辑器。
108    #[serde(rename = "table")]
109    Table,
110    /// 卡片式编辑器。
111    #[serde(rename = "cards")]
112    Cards,
113    /// 主从详情面板 — 左侧列表选择,右侧编辑详情。
114    #[serde(rename = "masterDetail")]
115    MasterDetail,
116    /// 搜索弹窗表格 — 通过 action 搜索并添加行。
117    #[serde(rename = "searchTable")]
118    SearchTable,
119}
120
121/// Schema 类型节点 — 描述数据的形状和校验规则。
122///
123/// 采用 tagged union 格式,通过 `type` 字段区分。
124/// 设计灵感来自 JSON Schema,但简化为只包含本项目需要的约束。
125#[derive(Debug, Clone, Serialize, Deserialize)]
126#[serde(tag = "type")]
127pub enum SchemaKind {
128    /// 字符串类型。
129    #[serde(rename = "string")]
130    String {
131        /// 枚举值列表,非空时限定输入只能从这些值中选择。
132        #[serde(rename = "enum", default)]
133        enum_values: Vec<String>,
134        /// 枚举值对应的可选展示标签,与 enum_values 等长。
135        /// 前端优先使用标签展示,缺失时回退到 enum_values 本身。
136        #[serde(rename = "enumLabels", default)]
137        enum_labels: Vec<String>,
138        /// 最小长度。
139        #[serde(rename = "minLength", default)]
140        min_length: Option<usize>,
141        /// 最大长度。
142        #[serde(rename = "maxLength", default)]
143        max_length: Option<usize>,
144        /// 正则表达式约束。
145        #[serde(rename = "pattern", default)]
146        pattern: Option<String>,
147    },
148    /// 浮点数类型。
149    #[serde(rename = "number")]
150    Number {
151        /// 最小值(含)。
152        #[serde(rename = "minimum", default)]
153        minimum: Option<f64>,
154        /// 最大值(含)。
155        #[serde(rename = "maximum", default)]
156        maximum: Option<f64>,
157        /// 步长约束(值必须是 multiple_of 的整数倍)。
158        #[serde(rename = "multipleOf", default)]
159        multiple_of: Option<f64>,
160    },
161    /// 整数类型。
162    #[serde(rename = "integer")]
163    Integer {
164        /// 最小值(含)。
165        #[serde(rename = "minimum", default)]
166        minimum: Option<i64>,
167        /// 最大值(含)。
168        #[serde(rename = "maximum", default)]
169        maximum: Option<i64>,
170        /// 步长约束。
171        #[serde(rename = "multipleOf", default)]
172        multiple_of: Option<i64>,
173    },
174    /// 布尔类型。
175    #[serde(rename = "boolean")]
176    Boolean,
177    /// 数组类型。
178    #[serde(rename = "array")]
179    Array {
180        /// 元素的 schema。
181        #[serde(rename = "items")]
182        items: Box<SchemaNode>,
183        /// 数组元素的 UI 提示,用于恢复 Path/Color 等 item-level 控件。
184        #[serde(rename = "itemWidget", default)]
185        item_widget: Option<WidgetHint>,
186        /// 最小元素数量。
187        #[serde(rename = "minItems", default)]
188        min_items: Option<usize>,
189        /// 最大元素数量。
190        #[serde(rename = "maxItems", default)]
191        max_items: Option<usize>,
192    },
193    /// 对象类型。
194    #[serde(rename = "object")]
195    Object {
196        /// 对象属性定义。
197        #[serde(rename = "properties")]
198        properties: BTreeMap<String, SchemaNode>,
199        /// 嵌套字段的 UI 元数据,与 properties 一一对应。
200        #[serde(rename = "ui", default)]
201        ui: Vec<FieldUiMetadata>,
202        /// 必需属性的 key 集合。
203        #[serde(rename = "required", default)]
204        required: BTreeSet<String>,
205    },
206}
207
208/// Schema 节点 — 包含类型定义和默认值。
209#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct SchemaNode {
211    /// 类型定义(flatten 到父级 JSON 中)。
212    #[serde(flatten)]
213    pub kind: SchemaKind,
214    /// 默认值。未设置时为 None。
215    #[serde(rename = "default", default)]
216    pub default: Option<Value>,
217}
218
219impl SchemaNode {
220    /// 创建一个无约束的字符串 schema 节点。
221    pub fn string() -> Self {
222        Self {
223            kind: SchemaKind::String {
224                enum_values: Vec::new(),
225                enum_labels: Vec::new(),
226                min_length: None,
227                max_length: None,
228                pattern: None,
229            },
230            default: None,
231        }
232    }
233
234    /// 创建一个无约束的浮点数 schema 节点。
235    pub fn number() -> Self {
236        Self {
237            kind: SchemaKind::Number {
238                minimum: None,
239                maximum: None,
240                multiple_of: None,
241            },
242            default: None,
243        }
244    }
245
246    /// 创建一个无约束的整数 schema 节点。
247    pub fn integer() -> Self {
248        Self {
249            kind: SchemaKind::Integer {
250                minimum: None,
251                maximum: None,
252                multiple_of: None,
253            },
254            default: None,
255        }
256    }
257
258    /// 创建一个布尔 schema 节点。
259    pub fn boolean() -> Self {
260        Self {
261            kind: SchemaKind::Boolean,
262            default: None,
263        }
264    }
265}
266
267/// 字段可见性条件 — 参照同级字段值动态显隐一个字段。
268///
269/// 语义:同一作用域(组件顶层,或对象条目内)中,字段 `field` 的当前值
270/// 与 `value` 全等时该字段可见。仅影响展示层渲染,不参与值校验与持久化。
271#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
272pub struct VisibleWhen {
273    /// 同级字段的 key(snake_case,与属性 key 一致)。
274    #[serde(rename = "field")]
275    pub field: String,
276    /// 目标值(与目标字段的 JSON 值全等比较)。
277    #[serde(rename = "value")]
278    pub value: Value,
279}
280
281/// 拖放投放声明 — 描述外部拖入的路径应写入本字段的哪个位置。
282///
283/// 仅由 `FieldUiMetadata::drop` 携带,经 `config_get_schema` 下发设置页,
284/// 由前端 `useFileDrop` 注册表消费;标量 `Path` 字段无需声明(按 widget 的 mode 推断)。
285#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
286pub struct DropSpec {
287    /// 接收路径的条目子字段名(数组元素须为 object)。
288    /// 拖入目录时写入该子字段;拖入文件时写入其父目录。
289    #[serde(rename = "pathField", default)]
290    pub path_field: String,
291    /// 接收文件模式的条目子字段名,写入形如 `*.exe` 的字符串。
292    /// None 表示文件投放只记录父目录、不合并模式。
293    #[serde(rename = "patternField", default)]
294    pub pattern_field: Option<String>,
295}
296
297/// 字段 UI 元数据 — 描述前端如何渲染和展示一个配置字段。
298///
299/// 通过 `pointer`(JSON Pointer 格式)关联到 `SettingsContribution.properties` 中的 schema 节点。
300#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct FieldUiMetadata {
302    /// 指向 schema 属性的 JSON Pointer(如 `"/theme"`)。
303    #[serde(rename = "pointer")]
304    pub pointer: String,
305    /// 字段显示标签。
306    #[serde(rename = "label")]
307    pub label: String,
308    /// 字段描述文本。
309    #[serde(rename = "description", default)]
310    pub description: String,
311    /// 字段补充说明(i18n key):阐述字段的作用范围、生效时机或特殊限制。
312    /// 界面以次要说明展示,并进入 AI 助手说明书;None 表示无需补充。
313    #[serde(rename = "note", default)]
314    pub note: Option<String>,
315    /// 分组名称,相同 group 的字段在前端渲染在同一区域。
316    #[serde(rename = "group", default)]
317    pub group: Option<String>,
318    /// 组内排序序号,越小越靠前。
319    #[serde(rename = "order", default)]
320    pub order: u32,
321    /// 是否可见。
322    #[serde(rename = "visible", default = "default_true")]
323    pub visible: bool,
324    /// 是否只读。
325    #[serde(rename = "readOnly", default)]
326    pub read_only: bool,
327    /// 是否为敏感字段(如 API Key)。敏感字段的值不进入 AI 助手上下文,
328    /// 读取时按占位符遮蔽;仅声明方(组件 schema)可标记。
329    #[serde(rename = "sensitive", default)]
330    pub sensitive: bool,
331    /// 可见性条件:Some 时仅当同级字段值匹配时可见;与 visible 叠加(两者都满足才可见)。
332    #[serde(rename = "visibleWhen", default)]
333    pub visible_when: Option<VisibleWhen>,
334    /// UI 控件提示。None 时前端根据 SchemaKind 选择默认控件。
335    #[serde(rename = "widget", default)]
336    pub widget: Option<WidgetHint>,
337    /// 运行时数据注入绑定。Some 时前端渲染搜索/检测按钮。
338    #[serde(rename = "action", default)]
339    pub action: Option<FieldAction>,
340    /// MasterDetail 详情面板联动动作定义。
341    /// 仅当 widget 为 MasterDetail 时有效。选中列表项时,
342    /// 前端调用指定的 config_action 获取预览数据,
343    /// 用户编辑结果写入 `targetField` 指定的兄弟设置字段。
344    #[serde(rename = "detailAction", default)]
345    pub detail_action: Option<DetailActionDef>,
346    /// 拖放投放声明。None 表示本字段不接受拖放。
347    #[serde(rename = "drop", default)]
348    pub drop: Option<DropSpec>,
349}
350
351fn default_true() -> bool {
352    true
353}
354
355/// 配置项定义 — 组件 `setting_schema()` 返回的单个配置字段描述。
356///
357/// 包含三部分:标识键、数据 schema、UI 元数据。
358/// 经 `SettingsContribution::from_entries()` 处理后拆分为 properties map + ui 数组。
359#[derive(Debug, Clone, Serialize, Deserialize)]
360pub struct SettingDefinition {
361    /// 配置项键名(snake_case),作为 settings JSON 中的 key。
362    #[serde(rename = "key")]
363    pub key: String,
364    /// 数据 schema 和校验规则。
365    #[serde(rename = "schema")]
366    pub schema: SchemaNode,
367    /// UI 呈现元数据。
368    #[serde(rename = "ui")]
369    pub ui: FieldUiMetadata,
370}
371
372/// 配置贡献 — 组件对外暴露的完整 schema 描述。
373///
374/// 包含 schema 版本号、属性定义(键 → schema)、UI 元数据列表、提交策略。
375/// 前端接收此结构后按需渲染表单、校验输入。
376#[derive(Debug, Clone, Serialize, Deserialize)]
377pub struct SettingsContribution {
378    /// Schema 版本号,用于向前兼容。
379    #[serde(rename = "schemaVersion")]
380    pub schema_version: u32,
381    /// 属性定义:字段 key → schema 节点。
382    #[serde(rename = "properties")]
383    pub properties: BTreeMap<String, SchemaNode>,
384    /// UI 元数据列表(每个字段一条)。
385    #[serde(rename = "ui")]
386    pub ui: Vec<FieldUiMetadata>,
387    /// 提交策略。
388    #[serde(rename = "commitPolicy", default)]
389    pub commit_policy: CommitPolicy,
390}
391
392impl SettingsContribution {
393    /// 从 `SettingDefinition` 列表构建 `SettingsContribution`。
394    ///
395    /// 校验 schema 合法性后,将 key+schema 拆入 properties map,ui 保留为数组。
396    pub fn from_entries(entries: Vec<SettingDefinition>) -> Result<Self, String> {
397        validate_setting_definitions(&entries)?;
398        let mut properties = BTreeMap::new();
399        let mut ui = Vec::with_capacity(entries.len());
400        for entry in entries {
401            properties.insert(entry.key, entry.schema);
402            ui.push(entry.ui);
403        }
404        ui.sort_by_key(|field| field.order);
405        Ok(Self {
406            schema_version: SETTINGS_SCHEMA_VERSION,
407            properties,
408            ui,
409            commit_policy: CommitPolicy::Staged,
410        })
411    }
412
413    /// 创建一个空的 SettingsContribution。
414    pub fn empty() -> Self {
415        Self {
416            schema_version: SETTINGS_SCHEMA_VERSION,
417            properties: BTreeMap::new(),
418            ui: Vec::new(),
419            commit_policy: CommitPolicy::Staged,
420        }
421    }
422
423    /// 从 schema 中收集所有默认值。
424    pub fn default_settings(&self) -> Value {
425        let values = self
426            .properties
427            .iter()
428            .filter_map(|(key, node)| {
429                let value = node.default.as_ref()?;
430                if value.is_null() {
431                    None
432                } else {
433                    Some((key.clone(), value.clone()))
434                }
435            })
436            .collect();
437        Value::Object(values)
438    }
439
440    /// 校验一个 settings JSON 值是否符合本 schema。
441    pub fn validate_values(&self, value: &Value) -> Result<(), String> {
442        let object = value
443            .as_object()
444            .ok_or_else(|| "settings root must be an object".to_string())?;
445        for key in object.keys() {
446            if !self.properties.contains_key(key) {
447                return Err(format!("unknown setting key: {}", key));
448            }
449        }
450        for (key, node) in &self.properties {
451            if let Some(value) = object.get(key) {
452                validate_node(node, value, &format!("/{}", escape_pointer(key)), 0)?;
453            }
454        }
455        Ok(())
456    }
457}
458
459// ── 校验函数 ──
460
461/// 校验一组 `SettingDefinition` 的合法性。
462pub fn validate_setting_definitions(entries: &[SettingDefinition]) -> Result<(), String> {
463    if entries.len() > 128 {
464        return Err("too many top-level settings (max 128)".to_string());
465    }
466    let mut keys = HashSet::new();
467    let top_keys: HashSet<&str> = entries.iter().map(|e| e.key.as_str()).collect();
468    for entry in entries {
469        if entry.key.is_empty() || entry.key.len() > 128 {
470            return Err("setting key length is invalid".to_string());
471        }
472        if !entry
473            .key
474            .bytes()
475            .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-' | b'.'))
476        {
477            return Err(format!("invalid setting key: {}", entry.key));
478        }
479        if !keys.insert(entry.key.clone()) {
480            return Err(format!("duplicate setting key: {}", entry.key));
481        }
482        let expected_pointer = format!("/{}", escape_pointer(&entry.key));
483        if entry.ui.pointer != expected_pointer {
484            return Err(format!(
485                "UI pointer '{}' does not match key '{}'",
486                entry.ui.pointer, entry.key
487            ));
488        }
489        validate_ui(&entry.ui)?;
490        check_visible_when(&entry.ui, &top_keys)?;
491        let mut node_count = 0usize;
492        validate_schema_node(&entry.schema, 0, &mut node_count)?;
493        if let Some(default) = &entry.schema.default {
494            validate_node(&entry.schema, default, &expected_pointer, 0)?;
495        }
496    }
497    Ok(())
498}
499
500/// 递归校验 schema 节点的结构、约束和嵌套深度。
501fn validate_schema_node(
502    node: &SchemaNode,
503    depth: usize,
504    node_count: &mut usize,
505) -> Result<(), String> {
506    if depth > 4 {
507        return Err("schema nesting exceeds limit (max 4)".to_string());
508    }
509    *node_count += 1;
510    if *node_count > 512 {
511        return Err("schema node count exceeds limit".to_string());
512    }
513    match &node.kind {
514        SchemaKind::String {
515            enum_values,
516            min_length,
517            max_length,
518            pattern,
519            ..
520        } => {
521            if enum_values.len() > 256 || enum_values.iter().any(|v| v.len() > 4096) {
522                return Err("string enum exceeds limit".to_string());
523            }
524            if let (Some(min), Some(max)) = (min_length, max_length) {
525                if min > max {
526                    return Err("minLength cannot exceed maxLength".to_string());
527                }
528            }
529            if pattern.as_ref().is_some_and(|p| p.len() > 512) {
530                return Err("pattern exceeds limit".to_string());
531            }
532            if let Some(pattern) = pattern {
533                if regex::Regex::new(pattern).is_err() {
534                    return Err("pattern is not a valid regular expression".to_string());
535                }
536            }
537        }
538        SchemaKind::Number {
539            minimum,
540            maximum,
541            multiple_of,
542        } => {
543            if let (Some(min), Some(max)) = (minimum, maximum) {
544                if min > max {
545                    return Err("minimum cannot exceed maximum".to_string());
546                }
547            }
548            if multiple_of.is_some_and(|v| v <= 0.0 || !v.is_finite()) {
549                return Err("multipleOf must be finite and positive".to_string());
550            }
551        }
552        SchemaKind::Integer {
553            minimum,
554            maximum,
555            multiple_of,
556        } => {
557            if let (Some(min), Some(max)) = (minimum, maximum) {
558                if min > max {
559                    return Err("minimum cannot exceed maximum".to_string());
560                }
561            }
562            if multiple_of.is_some_and(|v| v <= 0) {
563                return Err("multipleOf must be positive".to_string());
564            }
565        }
566        // item_widget 是 UI 渲染提示,不影响 schema 结构校验,此处无需关注。
567        SchemaKind::Array {
568            items,
569            min_items,
570            max_items,
571            ..
572        } => {
573            if max_items.unwrap_or(1024) > 1024 {
574                return Err("maxItems exceeds limit".to_string());
575            }
576            if let (Some(min), Some(max)) = (min_items, max_items) {
577                if min > max {
578                    return Err("minItems cannot exceed maxItems".to_string());
579                }
580            }
581            validate_schema_node(items, depth + 1, node_count)?;
582        }
583        SchemaKind::Object {
584            properties,
585            ui,
586            required,
587        } => {
588            validate_object_ui(properties, ui)?;
589            if properties.len() > 128 {
590                return Err("object property count exceeds limit".to_string());
591            }
592            for key in required {
593                if !properties.contains_key(key) {
594                    return Err(format!("required property does not exist: {}", key));
595                }
596            }
597            for (key, child) in properties {
598                if key.is_empty() || key.len() > 128 {
599                    return Err("invalid property key".to_string());
600                }
601                validate_schema_node(child, depth + 1, node_count)?;
602            }
603        }
604        SchemaKind::Boolean => {}
605    }
606    Ok(())
607}
608fn validate_node(
609    node: &SchemaNode,
610    value: &Value,
611    pointer: &str,
612    depth: usize,
613) -> Result<(), String> {
614    if depth > 4 {
615        return Err(format!("{} exceeds nesting limit", pointer));
616    }
617    match &node.kind {
618        SchemaKind::String {
619            enum_values,
620            min_length,
621            max_length,
622            pattern,
623            ..
624        } => {
625            let text = value
626                .as_str()
627                .ok_or_else(|| format!("{} must be a string", pointer))?;
628            let len = text.chars().count();
629            if let Some(min) = min_length {
630                if len < *min {
631                    return Err(format!("{} is too short (min {})", pointer, min));
632                }
633            }
634            if let Some(max) = max_length {
635                if len > *max {
636                    return Err(format!("{} is too long (max {})", pointer, max));
637                }
638            }
639            if !enum_values.is_empty() && !enum_values.iter().any(|v| v == text) {
640                return Err(format!("{} is not an allowed value", pointer));
641            }
642            if let Some(pattern) = pattern {
643                let mut cache = REGEX_CACHE.lock().unwrap();
644                let regex = cache.entry(pattern.clone()).or_insert_with(|| {
645                    Regex::new(pattern)
646                        .expect("pattern 已在 schema 构建时通过 validate_schema_node 校验")
647                });
648                if !regex.is_match(text) {
649                    return Err(format!("{} does not match pattern", pointer));
650                }
651            }
652        }
653        SchemaKind::Number {
654            minimum,
655            maximum,
656            multiple_of,
657        } => {
658            let number = value
659                .as_f64()
660                .filter(|n| n.is_finite())
661                .ok_or_else(|| format!("{} must be a finite number", pointer))?;
662            if let Some(min) = minimum {
663                if number < *min {
664                    return Err(format!("{} is below minimum {}", pointer, min));
665                }
666            }
667            if let Some(max) = maximum {
668                if number > *max {
669                    return Err(format!("{} is above maximum {}", pointer, max));
670                }
671            }
672            if let Some(step) = multiple_of {
673                let quotient = number / step;
674                // JSON 浮点值经 f32 往返后存在舍入误差,按 schema 精度容忍误差。
675                if (quotient - quotient.round()).abs() > 1e-6 {
676                    return Err(format!("{} is not a multiple of {}", pointer, step));
677                }
678            }
679        }
680        SchemaKind::Integer {
681            minimum,
682            maximum,
683            multiple_of,
684        } => {
685            let number = value
686                .as_f64()
687                .filter(|v| v.is_finite() && v.fract() == 0.0)
688                .filter(|v| *v >= i64::MIN as f64 && *v <= i64::MAX as f64)
689                .map(|v| v as i64)
690                .ok_or_else(|| format!("{} must be an integer", pointer))?;
691            if let Some(min) = minimum {
692                if number < *min {
693                    return Err(format!("{} is below minimum {}", pointer, min));
694                }
695            }
696            if let Some(max) = maximum {
697                if number > *max {
698                    return Err(format!("{} is above maximum {}", pointer, max));
699                }
700            }
701            if let Some(step) = multiple_of {
702                if number % step != 0 {
703                    return Err(format!("{} is not a multiple of {}", pointer, step));
704                }
705            }
706        }
707        SchemaKind::Boolean => {
708            if !value.is_boolean() {
709                return Err(format!("{} must be a boolean", pointer));
710            }
711        }
712        // item_widget 是 UI 渲染提示,不影响值校验,此处无需关注。
713        SchemaKind::Array {
714            items,
715            min_items,
716            max_items,
717            ..
718        } => {
719            let array = value
720                .as_array()
721                .ok_or_else(|| format!("{} must be an array", pointer))?;
722            if let Some(min) = min_items {
723                if array.len() < *min {
724                    return Err(format!("{} has too few items (min {})", pointer, min));
725                }
726            }
727            if let Some(max) = max_items {
728                if array.len() > *max {
729                    return Err(format!("{} has too many items (max {})", pointer, max));
730                }
731            }
732            for (index, item) in array.iter().enumerate() {
733                validate_node(items, item, &format!("{}/{}", pointer, index), depth + 1)?;
734            }
735        }
736        SchemaKind::Object {
737            properties,
738            required,
739            ..
740        } => {
741            let object = value
742                .as_object()
743                .ok_or_else(|| format!("{} must be an object", pointer))?;
744            for key in required {
745                if !object.contains_key(key) {
746                    return Err(format!("{}/{} is required", pointer, escape_pointer(key)));
747                }
748            }
749            for key in object.keys() {
750                if !properties.contains_key(key) {
751                    return Err(format!(
752                        "unknown property: {}/{}",
753                        pointer,
754                        escape_pointer(key)
755                    ));
756                }
757            }
758            for (key, child) in properties {
759                if let Some(child_value) = object.get(key) {
760                    validate_node(
761                        child,
762                        child_value,
763                        &format!("{}/{}", pointer, escape_pointer(key)),
764                        depth + 1,
765                    )?;
766                }
767            }
768        }
769    }
770    Ok(())
771}
772fn validate_ui(ui: &FieldUiMetadata) -> Result<(), String> {
773    if ui.label.is_empty() || ui.label.len() > 512 || ui.description.len() > 4096 {
774        return Err(format!("invalid UI metadata at {}", ui.pointer));
775    }
776    if ui.group.as_ref().is_some_and(|g| g.len() > 512) {
777        return Err(format!("UI group exceeds limit at {}", ui.pointer));
778    }
779    // 补充说明声明了就必须有内容:空串会让界面渲染出一个空气泡、说明书多一段空的「补充」。
780    if ui
781        .note
782        .as_ref()
783        .is_some_and(|note| note.is_empty() || note.len() > 4096)
784    {
785        return Err(format!("invalid UI note at {}", ui.pointer));
786    }
787    Ok(())
788}
789
790/// 校验 visible_when 引用的字段存在于同一作用域(同级字段集合)。
791fn check_visible_when(ui: &FieldUiMetadata, sibling_keys: &HashSet<&str>) -> Result<(), String> {
792    if let Some(cond) = &ui.visible_when {
793        if cond.field.is_empty() || cond.field.len() > 128 {
794            return Err(format!("invalid visibleWhen field at {}", ui.pointer));
795        }
796        if !sibling_keys.contains(cond.field.as_str()) {
797            return Err(format!(
798                "visibleWhen field '{}' does not exist in the same scope (pointer {})",
799                cond.field, ui.pointer
800            ));
801        }
802    }
803    Ok(())
804}
805/// 校验嵌套 object 的 UI 元数据是否与 properties 完整对应。
806fn validate_object_ui(
807    properties: &BTreeMap<String, SchemaNode>,
808    ui: &[FieldUiMetadata],
809) -> Result<(), String> {
810    if ui.len() != properties.len() {
811        return Err("object UI metadata must match properties".to_string());
812    }
813    let mut seen = HashSet::new();
814    let sibling_keys: HashSet<&str> = properties.keys().map(|k| k.as_str()).collect();
815    for metadata in ui {
816        validate_ui(metadata)?;
817        check_visible_when(metadata, &sibling_keys)?;
818        if !seen.insert(metadata.pointer.clone()) {
819            return Err(format!("duplicate object UI pointer: {}", metadata.pointer));
820        }
821        let matches_property = properties
822            .keys()
823            .any(|key| format!("/{}", escape_pointer(key)) == metadata.pointer);
824        if !matches_property {
825            return Err(format!(
826                "object UI pointer does not match properties: {}",
827                metadata.pointer
828            ));
829        }
830    }
831    Ok(())
832}
833
834fn escape_pointer(value: &str) -> String {
835    value.replace('~', "~0").replace('/', "~1")
836}
837
838/// 原始类型枚举,用于 builder 的 `primitive_item()` 方法,
839/// 快速指定数组元素的数据类型。
840///
841/// 不参与 JSON 序列化,调用后立即展开为 `SchemaNode`。
842/// ——调用方只需说"我要 text/number/integer/boolean 类型的数组元素",
843///
844/// 覆盖所有合理的数组元素类型:
845/// - 字符串(Text / Path / Color / Select)
846/// - 数值(Number / Integer)
847/// - 布尔(Boolean)
848#[derive(Debug, Clone)]
849pub enum PrimitiveType {
850    /// 字符串。
851    Text,
852    /// 浮点数,附带可选的范围/步长约束。
853    Number {
854        /// 最小值。
855        min: Option<f64>,
856        /// 最大值。
857        max: Option<f64>,
858        /// 步长。
859        step: Option<f64>,
860    },
861    /// 整数,附带可选的范围/步长约束。
862    Integer {
863        /// 最小值。
864        min: Option<i64>,
865        /// 最大值。
866        max: Option<i64>,
867        /// 步长。
868        step: Option<i64>,
869    },
870    /// 布尔值。
871    Boolean,
872    /// 单选下拉,附带可选值列表。
873    Select {
874        /// 可选值列表。
875        options: Vec<String>,
876    },
877    /// 路径选择。
878    Path {
879        /// 选择模式。
880        mode: PathMode,
881    },
882    /// 颜色。
883    Color,
884}
885
886#[cfg(test)]
887mod tests {
888    use super::*;
889
890    #[test]
891    fn rejects_unknown_settings() {
892        let contribution = SettingsContribution::from_entries(vec![SettingDefinition {
893            key: "enabled".into(),
894            schema: SchemaNode {
895                kind: SchemaKind::Boolean,
896                default: Some(Value::Bool(true)),
897            },
898            ui: FieldUiMetadata {
899                pointer: "/enabled".into(),
900                label: "Enabled".into(),
901                description: String::new(),
902                note: None,
903                group: None,
904                order: 0,
905                visible: true,
906                read_only: false,
907                sensitive: false,
908                visible_when: None,
909                widget: None,
910                action: None,
911                detail_action: None,
912                drop: None,
913            },
914        }])
915        .unwrap();
916        assert!(contribution
917            .validate_values(&serde_json::json!({"other": true}))
918            .is_err());
919    }
920
921    #[test]
922    fn rejects_mismatched_ui_pointer() {
923        let result = SettingsContribution::from_entries(vec![SettingDefinition {
924            key: "enabled".into(),
925            schema: SchemaNode::boolean(),
926            ui: FieldUiMetadata {
927                pointer: "/other".into(),
928                label: "Enabled".into(),
929                description: String::new(),
930                note: None,
931                group: None,
932                order: 0,
933                visible: true,
934                read_only: false,
935                sensitive: false,
936                visible_when: None,
937                widget: None,
938                action: None,
939                detail_action: None,
940                drop: None,
941            },
942        }]);
943        assert!(result.is_err());
944    }
945
946    #[test]
947    fn collects_defaults() {
948        let contribution = SettingsContribution::from_entries(vec![SettingDefinition {
949            key: "theme".into(),
950            schema: SchemaNode {
951                kind: SchemaKind::String {
952                    enum_values: vec!["light".into(), "dark".into()],
953                    enum_labels: Vec::new(),
954                    min_length: None,
955                    max_length: None,
956                    pattern: None,
957                },
958                default: Some(Value::String("light".into())),
959            },
960            ui: FieldUiMetadata {
961                pointer: "/theme".into(),
962                label: "Theme".into(),
963                description: String::new(),
964                note: None,
965                group: None,
966                order: 0,
967                visible: true,
968                read_only: false,
969                sensitive: false,
970                visible_when: None,
971                widget: None,
972                action: None,
973                detail_action: None,
974                drop: None,
975            },
976        }])
977        .unwrap();
978        let defaults = contribution.default_settings();
979        assert_eq!(defaults, serde_json::json!({"theme": "light"}));
980    }
981    /// 构造测试用的最小字段 UI 元数据。
982    fn test_ui(pointer: &str) -> FieldUiMetadata {
983        FieldUiMetadata {
984            pointer: pointer.into(),
985            label: pointer.trim_start_matches('/').into(),
986            description: String::new(),
987            note: None,
988            group: None,
989            order: 0,
990            visible: true,
991            read_only: false,
992            sensitive: false,
993            visible_when: None,
994            widget: None,
995            action: None,
996            detail_action: None,
997            drop: None,
998        }
999    }
1000
1001    /// 验证嵌套 object 按完整 schema 递归校验并接受合法默认值。
1002    #[test]
1003    fn validates_visible_when_sibling_scope() {
1004        let kind = SettingDefinition {
1005            key: "kind".into(),
1006            schema: SchemaNode::string(),
1007            ui: test_ui("/kind"),
1008        };
1009        let mut temp = SettingDefinition {
1010            key: "temp".into(),
1011            schema: SchemaNode::number(),
1012            ui: test_ui("/temp"),
1013        };
1014        temp.ui.visible_when = Some(VisibleWhen {
1015            field: "kind".into(),
1016            value: Value::String("chat".into()),
1017        });
1018        assert!(SettingsContribution::from_entries(vec![kind.clone(), temp.clone()]).is_ok());
1019
1020        temp.ui.visible_when = Some(VisibleWhen {
1021            field: "missing".into(),
1022            value: Value::String("chat".into()),
1023        });
1024        assert!(SettingsContribution::from_entries(vec![kind, temp]).is_err());
1025    }
1026
1027    #[test]
1028    fn validates_nested_object_schema() {
1029        let mut properties = BTreeMap::new();
1030        properties.insert("name".into(), SchemaNode::string());
1031        let schema = SchemaNode {
1032            kind: SchemaKind::Object {
1033                properties,
1034                ui: vec![test_ui("/name")],
1035                required: BTreeSet::from(["name".into()]),
1036            },
1037            default: Some(serde_json::json!({"name": "default"})),
1038        };
1039        let contribution = SettingsContribution::from_entries(vec![SettingDefinition {
1040            key: "profile".into(),
1041            schema,
1042            ui: test_ui("/profile"),
1043        }])
1044        .unwrap();
1045        assert!(contribution
1046            .validate_values(&serde_json::json!({
1047                "profile": {"name": "value"}
1048            }))
1049            .is_ok());
1050    }
1051
1052    /// 验证嵌套 object 缺失 UI metadata 时被拒绝而不是静默降级。
1053    #[test]
1054    fn rejects_nested_object_without_ui_metadata() {
1055        let mut properties = BTreeMap::new();
1056        properties.insert("name".into(), SchemaNode::string());
1057        let result = SettingsContribution::from_entries(vec![SettingDefinition {
1058            key: "profile".into(),
1059            schema: SchemaNode {
1060                kind: SchemaKind::Object {
1061                    properties,
1062                    ui: Vec::new(),
1063                    required: BTreeSet::new(),
1064                },
1065                default: None,
1066            },
1067            ui: test_ui("/profile"),
1068        }]);
1069        assert!(result.is_err());
1070    }
1071}