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