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
9static REGEX_CACHE: Lazy<Mutex<HashMap<String, Regex>>> = Lazy::new(|| Mutex::new(HashMap::new()));
12pub const SETTINGS_SCHEMA_VERSION: u32 = 1;
14
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
16pub enum CommitPolicy {
17 #[default]
19 #[serde(rename = "staged")]
20 Staged,
21 #[serde(rename = "immediateAllowed")]
23 ImmediateAllowed,
24}
25
26#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
28pub enum PathMode {
29 #[serde(rename = "file")]
31 File,
32 #[serde(rename = "directory")]
34 Directory,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
42#[serde(tag = "kind")]
43pub enum WidgetHint {
44 #[serde(rename = "text")]
46 Text,
47 #[serde(rename = "textarea")]
49 Textarea,
50 #[serde(rename = "number")]
52 Number,
53 #[serde(rename = "toggle")]
55 Toggle,
56 #[serde(rename = "select")]
58 Select,
59 #[serde(rename = "multiselect")]
61 MultiSelect,
62 #[serde(rename = "path")]
64 Path {
65 #[serde(rename = "mode")]
67 mode: PathMode,
68 },
69 #[serde(rename = "color")]
71 Color,
72 #[serde(rename = "image")]
74 Image {
75 #[serde(rename = "accept")]
77 accept: Vec<String>,
78 #[serde(rename = "maxSize")]
80 #[serde(default)]
81 max_size: Option<u64>,
82 },
83 #[serde(rename = "font")]
85 Font {
86 #[serde(rename = "action")]
88 action: String,
89 #[serde(rename = "component", default)]
91 component: Option<String>,
92 },
93 #[serde(rename = "hotkey")]
95 Hotkey,
96 #[serde(rename = "list")]
98 List,
99 #[serde(rename = "tags")]
101 Tags,
102 #[serde(rename = "table")]
104 Table,
105 #[serde(rename = "cards")]
107 Cards,
108 #[serde(rename = "masterDetail")]
110 MasterDetail,
111 #[serde(rename = "searchTable")]
113 SearchTable,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
121#[serde(tag = "type")]
122pub enum SchemaKind {
123 #[serde(rename = "string")]
125 String {
126 #[serde(rename = "enum", default)]
128 enum_values: Vec<String>,
129 #[serde(rename = "enumLabels", default)]
132 enum_labels: Vec<String>,
133 #[serde(rename = "minLength", default)]
135 min_length: Option<usize>,
136 #[serde(rename = "maxLength", default)]
138 max_length: Option<usize>,
139 #[serde(rename = "pattern", default)]
141 pattern: Option<String>,
142 },
143 #[serde(rename = "number")]
145 Number {
146 #[serde(rename = "minimum", default)]
148 minimum: Option<f64>,
149 #[serde(rename = "maximum", default)]
151 maximum: Option<f64>,
152 #[serde(rename = "multipleOf", default)]
154 multiple_of: Option<f64>,
155 },
156 #[serde(rename = "integer")]
158 Integer {
159 #[serde(rename = "minimum", default)]
161 minimum: Option<i64>,
162 #[serde(rename = "maximum", default)]
164 maximum: Option<i64>,
165 #[serde(rename = "multipleOf", default)]
167 multiple_of: Option<i64>,
168 },
169 #[serde(rename = "boolean")]
171 Boolean,
172 #[serde(rename = "array")]
174 Array {
175 #[serde(rename = "items")]
177 items: Box<SchemaNode>,
178 #[serde(rename = "itemWidget", default)]
180 item_widget: Option<WidgetHint>,
181 #[serde(rename = "minItems", default)]
183 min_items: Option<usize>,
184 #[serde(rename = "maxItems", default)]
186 max_items: Option<usize>,
187 },
188 #[serde(rename = "object")]
190 Object {
191 #[serde(rename = "properties")]
193 properties: BTreeMap<String, SchemaNode>,
194 #[serde(rename = "ui", default)]
196 ui: Vec<FieldUiMetadata>,
197 #[serde(rename = "required", default)]
199 required: BTreeSet<String>,
200 },
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct SchemaNode {
206 #[serde(flatten)]
208 pub kind: SchemaKind,
209 #[serde(rename = "default", default)]
211 pub default: Option<Value>,
212}
213
214impl SchemaNode {
215 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 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 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 pub fn boolean() -> Self {
255 Self {
256 kind: SchemaKind::Boolean,
257 default: None,
258 }
259 }
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
267pub struct VisibleWhen {
268 #[serde(rename = "field")]
270 pub field: String,
271 #[serde(rename = "value")]
273 pub value: Value,
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize)]
280pub struct FieldUiMetadata {
281 #[serde(rename = "pointer")]
283 pub pointer: String,
284 #[serde(rename = "label")]
286 pub label: String,
287 #[serde(rename = "description", default)]
289 pub description: String,
290 #[serde(rename = "group", default)]
292 pub group: Option<String>,
293 #[serde(rename = "order", default)]
295 pub order: u32,
296 #[serde(rename = "visible", default = "default_true")]
298 pub visible: bool,
299 #[serde(rename = "readOnly", default)]
301 pub read_only: bool,
302 #[serde(rename = "visibleWhen", default)]
304 pub visible_when: Option<VisibleWhen>,
305 #[serde(rename = "widget", default)]
307 pub widget: Option<WidgetHint>,
308 #[serde(rename = "action", default)]
310 pub action: Option<FieldAction>,
311 #[serde(rename = "detailAction", default)]
316 pub detail_action: Option<DetailActionDef>,
317}
318
319fn default_true() -> bool {
320 true
321}
322
323#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct SettingDefinition {
329 #[serde(rename = "key")]
331 pub key: String,
332 #[serde(rename = "schema")]
334 pub schema: SchemaNode,
335 #[serde(rename = "ui")]
337 pub ui: FieldUiMetadata,
338}
339
340#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct SettingsContribution {
346 #[serde(rename = "schemaVersion")]
348 pub schema_version: u32,
349 #[serde(rename = "properties")]
351 pub properties: BTreeMap<String, SchemaNode>,
352 #[serde(rename = "ui")]
354 pub ui: Vec<FieldUiMetadata>,
355 #[serde(rename = "commitPolicy", default)]
357 pub commit_policy: CommitPolicy,
358}
359
360impl SettingsContribution {
361 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 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 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 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
427pub 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
468fn 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 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 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 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
750fn 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}
765fn 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#[derive(Debug, Clone)]
809pub enum PrimitiveType {
810 Text,
812 Number {
814 min: Option<f64>,
816 max: Option<f64>,
818 step: Option<f64>,
820 },
821 Integer {
823 min: Option<i64>,
825 max: Option<i64>,
827 step: Option<i64>,
829 },
830 Boolean,
832 Select {
834 options: Vec<String>,
836 },
837 Path {
839 mode: PathMode,
841 },
842 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 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 #[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 #[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}