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