1use rust_i18n::t;
56use serde::Deserialize;
57use std::collections::HashMap;
58
59#[derive(Debug, Clone)]
61pub struct SettingSchema {
62 pub path: String,
64 pub name: String,
66 pub description: Option<String>,
68 pub setting_type: SettingType,
70 pub default: Option<serde_json::Value>,
72 pub read_only: bool,
74 pub section: Option<String>,
76 pub order: Option<i32>,
79}
80
81#[derive(Debug, Clone)]
83pub enum SettingType {
84 Boolean,
86 Integer {
88 minimum: Option<i64>,
89 maximum: Option<i64>,
90 },
91 Number {
93 minimum: Option<f64>,
94 maximum: Option<f64>,
95 },
96 String,
98 Enum { options: Vec<EnumOption> },
100 StringArray,
102 IntegerArray,
104 ObjectArray {
106 item_schema: Box<SettingSchema>,
107 display_field: Option<String>,
109 },
110 Object { properties: Vec<SettingSchema> },
112 Map {
114 value_schema: Box<SettingSchema>,
115 display_field: Option<String>,
117 no_add: bool,
119 },
120 Complex,
122}
123
124#[derive(Debug, Clone)]
126pub struct EnumOption {
127 pub name: String,
129 pub value: String,
131}
132
133#[derive(Debug, Clone)]
135pub struct SettingCategory {
136 pub name: String,
138 pub path: String,
140 pub description: Option<String>,
142 pub settings: Vec<SettingSchema>,
144 pub subcategories: Vec<SettingCategory>,
146}
147
148#[derive(Debug, Deserialize)]
150struct RawSchema {
151 #[serde(rename = "type")]
152 schema_type: Option<SchemaType>,
153 description: Option<String>,
154 default: Option<serde_json::Value>,
155 properties: Option<HashMap<String, RawSchema>>,
156 items: Option<Box<RawSchema>>,
157 #[serde(rename = "enum")]
158 enum_values: Option<Vec<serde_json::Value>>,
159 minimum: Option<serde_json::Number>,
160 maximum: Option<serde_json::Number>,
161 #[serde(rename = "$ref")]
162 ref_path: Option<String>,
163 #[serde(rename = "$defs")]
164 defs: Option<HashMap<String, RawSchema>>,
165 #[serde(rename = "additionalProperties")]
166 additional_properties: Option<AdditionalProperties>,
167 #[serde(rename = "x-enum-values", default)]
169 extensible_enum_values: Vec<EnumValueEntry>,
170 #[serde(rename = "x-display-field")]
173 display_field: Option<String>,
174 #[serde(rename = "readOnly", default)]
176 read_only: bool,
177 #[serde(rename = "x-standalone-category", default)]
179 standalone_category: bool,
180 #[serde(rename = "x-no-add", default)]
182 no_add: bool,
183 #[serde(rename = "x-section")]
185 section: Option<String>,
186 #[serde(rename = "x-order")]
188 order: Option<i32>,
189}
190
191#[derive(Debug, Deserialize)]
193struct EnumValueEntry {
194 #[serde(rename = "ref")]
196 ref_path: String,
197 name: Option<String>,
199 value: serde_json::Value,
201}
202
203#[derive(Debug, Deserialize)]
205#[serde(untagged)]
206enum AdditionalProperties {
207 Bool(bool),
208 Schema(Box<RawSchema>),
209}
210
211#[derive(Debug, Deserialize)]
213#[serde(untagged)]
214enum SchemaType {
215 Single(String),
216 Multiple(Vec<String>),
217}
218
219impl SchemaType {
220 fn primary(&self) -> Option<&str> {
222 match self {
223 Self::Single(s) => Some(s.as_str()),
224 Self::Multiple(v) => v.first().map(|s| s.as_str()),
225 }
226 }
227}
228
229type EnumValuesMap = HashMap<String, Vec<EnumOption>>;
231
232pub fn parse_schema(schema_json: &str) -> Result<Vec<SettingCategory>, serde_json::Error> {
234 let raw: RawSchema = serde_json::from_str(schema_json)?;
235
236 let defs = raw.defs.unwrap_or_default();
237 let properties = raw.properties.unwrap_or_default();
238
239 let enum_values_map = build_enum_values_map(&raw.extensible_enum_values);
241
242 let mut categories = Vec::new();
243 let mut top_level_settings = Vec::new();
244
245 let mut sorted_props: Vec<_> = properties.into_iter().collect();
247 sorted_props.sort_by(|a, b| a.0.cmp(&b.0));
248 for (name, prop) in sorted_props {
249 let path = format!("/{}", name);
250 let display_name = humanize_name(&name);
251
252 let resolved = resolve_ref(&prop, &defs);
254
255 if prop.standalone_category {
257 let setting = parse_setting(&name, &path, &prop, &defs, &enum_values_map);
259 categories.push(SettingCategory {
260 name: display_name,
261 path: path.clone(),
262 description: prop.description.clone().or(resolved.description.clone()),
263 settings: vec![setting],
264 subcategories: Vec::new(),
265 });
266 } else if let Some(ref inner_props) = resolved.properties {
267 let settings = parse_properties(inner_props, &path, &defs, &enum_values_map);
269 categories.push(SettingCategory {
270 name: display_name,
271 path: path.clone(),
272 description: resolved.description.clone(),
273 settings,
274 subcategories: Vec::new(),
275 });
276 } else {
277 let setting = parse_setting(&name, &path, &prop, &defs, &enum_values_map);
279 top_level_settings.push(setting);
280 }
281 }
282
283 if !top_level_settings.is_empty() {
285 top_level_settings.sort_by(|a, b| a.name.cmp(&b.name));
287 categories.insert(
288 0,
289 SettingCategory {
290 name: "General".to_string(),
291 path: String::new(),
292 description: Some("General settings".to_string()),
293 settings: top_level_settings,
294 subcategories: Vec::new(),
295 },
296 );
297 }
298
299 categories.sort_by(|a, b| match (a.name.as_str(), b.name.as_str()) {
301 ("General", _) => std::cmp::Ordering::Less,
302 (_, "General") => std::cmp::Ordering::Greater,
303 (a, b) => a.cmp(b),
304 });
305
306 Ok(categories)
307}
308
309fn build_enum_values_map(entries: &[EnumValueEntry]) -> EnumValuesMap {
311 let mut map: EnumValuesMap = HashMap::new();
312
313 for entry in entries {
314 let value_str = match &entry.value {
315 serde_json::Value::String(s) => s.clone(),
316 other => other.to_string(),
317 };
318
319 let option = EnumOption {
320 name: entry.name.clone().unwrap_or_else(|| value_str.clone()),
321 value: value_str,
322 };
323
324 map.entry(entry.ref_path.clone()).or_default().push(option);
325 }
326
327 map
328}
329
330fn parse_properties(
332 properties: &HashMap<String, RawSchema>,
333 parent_path: &str,
334 defs: &HashMap<String, RawSchema>,
335 enum_values_map: &EnumValuesMap,
336) -> Vec<SettingSchema> {
337 let mut settings = Vec::new();
338
339 for (name, prop) in properties {
340 let path = format!("{}/{}", parent_path, name);
341 let setting = parse_setting(name, &path, prop, defs, enum_values_map);
342 settings.push(setting);
343 }
344
345 settings.sort_by(|a, b| match (a.order, b.order) {
348 (Some(a_ord), Some(b_ord)) => a_ord.cmp(&b_ord).then_with(|| a.name.cmp(&b.name)),
349 (Some(_), None) => std::cmp::Ordering::Less,
350 (None, Some(_)) => std::cmp::Ordering::Greater,
351 (None, None) => a.name.cmp(&b.name),
352 });
353
354 settings
355}
356
357fn parse_setting(
359 name: &str,
360 path: &str,
361 schema: &RawSchema,
362 defs: &HashMap<String, RawSchema>,
363 enum_values_map: &EnumValuesMap,
364) -> SettingSchema {
365 let setting_type = determine_type(schema, defs, enum_values_map);
366
367 let resolved = resolve_ref(schema, defs);
369 let description = schema
370 .description
371 .clone()
372 .or_else(|| resolved.description.clone());
373
374 let read_only = schema.read_only || resolved.read_only;
376
377 let section = schema.section.clone().or_else(|| resolved.section.clone());
379
380 let order = schema.order.or(resolved.order);
382
383 SettingSchema {
384 path: path.to_string(),
385 name: i18n_name(path, name),
386 description,
387 setting_type,
388 default: schema.default.clone(),
389 read_only,
390 section,
391 order,
392 }
393}
394
395fn determine_type(
397 schema: &RawSchema,
398 defs: &HashMap<String, RawSchema>,
399 enum_values_map: &EnumValuesMap,
400) -> SettingType {
401 if let Some(ref ref_path) = schema.ref_path {
403 if let Some(options) = enum_values_map.get(ref_path) {
404 if !options.is_empty() {
405 return SettingType::Enum {
406 options: options.clone(),
407 };
408 }
409 }
410 }
411
412 let resolved = resolve_ref(schema, defs);
414
415 let enum_values = schema
417 .enum_values
418 .as_ref()
419 .or(resolved.enum_values.as_ref());
420 if let Some(values) = enum_values {
421 let options: Vec<EnumOption> = values
422 .iter()
423 .filter_map(|v| {
424 if v.is_null() {
425 Some(EnumOption {
427 name: "Auto-detect".to_string(),
428 value: String::new(), })
430 } else {
431 v.as_str().map(|s| EnumOption {
432 name: s.to_string(),
433 value: s.to_string(),
434 })
435 }
436 })
437 .collect();
438 if !options.is_empty() {
439 return SettingType::Enum { options };
440 }
441 }
442
443 match resolved.schema_type.as_ref().and_then(|t| t.primary()) {
445 Some("boolean") => SettingType::Boolean,
446 Some("integer") => {
447 let minimum = resolved.minimum.as_ref().and_then(|n| n.as_i64());
448 let maximum = resolved.maximum.as_ref().and_then(|n| n.as_i64());
449 SettingType::Integer { minimum, maximum }
450 }
451 Some("number") => {
452 let minimum = resolved.minimum.as_ref().and_then(|n| n.as_f64());
453 let maximum = resolved.maximum.as_ref().and_then(|n| n.as_f64());
454 SettingType::Number { minimum, maximum }
455 }
456 Some("string") => SettingType::String,
457 Some("array") => {
458 if let Some(ref items) = resolved.items {
460 let item_resolved = resolve_ref(items, defs);
461 let item_type = item_resolved.schema_type.as_ref().and_then(|t| t.primary());
462 if item_type == Some("string") {
463 return SettingType::StringArray;
464 }
465 if item_type == Some("integer") || item_type == Some("number") {
466 return SettingType::IntegerArray;
467 }
468 if items.ref_path.is_some() {
470 let item_schema =
472 parse_setting("item", "", item_resolved, defs, enum_values_map);
473
474 if matches!(item_schema.setting_type, SettingType::Object { .. }) {
476 let display_field = item_resolved.display_field.clone();
478 return SettingType::ObjectArray {
479 item_schema: Box::new(item_schema),
480 display_field,
481 };
482 }
483 }
484 }
485 SettingType::Complex
486 }
487 Some("object") => {
488 if let Some(ref add_props) = resolved.additional_properties {
490 match add_props {
491 AdditionalProperties::Schema(schema_box) => {
492 let inner_resolved = resolve_ref(schema_box, defs);
493 let value_schema =
494 parse_setting("value", "", inner_resolved, defs, enum_values_map);
495
496 let display_field = inner_resolved.display_field.clone().or_else(|| {
499 inner_resolved.items.as_ref().and_then(|items| {
500 let items_resolved = resolve_ref(items, defs);
501 items_resolved.display_field.clone()
502 })
503 });
504
505 let no_add = resolved.no_add;
507
508 return SettingType::Map {
509 value_schema: Box::new(value_schema),
510 display_field,
511 no_add,
512 };
513 }
514 AdditionalProperties::Bool(true) => {
515 return SettingType::Complex;
517 }
518 AdditionalProperties::Bool(false) => {
519 }
522 }
523 }
524 if let Some(ref props) = resolved.properties {
526 let properties = parse_properties(props, "", defs, enum_values_map);
527 return SettingType::Object { properties };
528 }
529 SettingType::Complex
530 }
531 _ => SettingType::Complex,
532 }
533}
534
535fn resolve_ref<'a>(schema: &'a RawSchema, defs: &'a HashMap<String, RawSchema>) -> &'a RawSchema {
537 if let Some(ref ref_path) = schema.ref_path {
538 if let Some(def_name) = ref_path.strip_prefix("#/$defs/") {
540 if let Some(def) = defs.get(def_name) {
541 return def;
542 }
543 }
544 }
545 schema
546}
547
548fn i18n_name(path: &str, fallback_name: &str) -> String {
554 let key = format!("settings.field{}", path.replace('/', "."));
555 let translated = t!(&key);
556 if *translated == key {
557 humanize_name(fallback_name)
558 } else {
559 translated.to_string()
560 }
561}
562
563fn humanize_name(name: &str) -> String {
565 name.split('_')
566 .map(|word| {
567 let mut chars = word.chars();
568 match chars.next() {
569 None => String::new(),
570 Some(first) => first.to_uppercase().chain(chars).collect(),
571 }
572 })
573 .collect::<Vec<_>>()
574 .join(" ")
575}
576
577#[cfg(test)]
578mod tests {
579 use super::*;
580
581 const SAMPLE_SCHEMA: &str = r##"
582{
583 "$schema": "https://json-schema.org/draft/2020-12/schema",
584 "title": "Config",
585 "type": "object",
586 "properties": {
587 "theme": {
588 "description": "Color theme name",
589 "type": "string",
590 "default": "high-contrast"
591 },
592 "check_for_updates": {
593 "description": "Check for new versions on quit",
594 "type": "boolean",
595 "default": true
596 },
597 "editor": {
598 "description": "Editor settings",
599 "$ref": "#/$defs/EditorConfig"
600 }
601 },
602 "$defs": {
603 "EditorConfig": {
604 "description": "Editor behavior configuration",
605 "type": "object",
606 "properties": {
607 "tab_size": {
608 "description": "Number of spaces per tab",
609 "type": "integer",
610 "minimum": 1,
611 "maximum": 16,
612 "default": 4
613 },
614 "line_numbers": {
615 "description": "Show line numbers",
616 "type": "boolean",
617 "default": true
618 }
619 }
620 }
621 }
622}
623"##;
624
625 #[test]
626 fn test_parse_schema() {
627 let categories = parse_schema(SAMPLE_SCHEMA).unwrap();
628
629 assert_eq!(categories.len(), 2);
631 assert_eq!(categories[0].name, "General");
632 assert_eq!(categories[1].name, "Editor");
633 }
634
635 #[test]
636 fn test_general_category() {
637 let categories = parse_schema(SAMPLE_SCHEMA).unwrap();
638 let general = &categories[0];
639
640 assert_eq!(general.settings.len(), 2);
642
643 let theme = general
644 .settings
645 .iter()
646 .find(|s| s.path == "/theme")
647 .unwrap();
648 assert!(matches!(theme.setting_type, SettingType::String));
649
650 let updates = general
651 .settings
652 .iter()
653 .find(|s| s.path == "/check_for_updates")
654 .unwrap();
655 assert!(matches!(updates.setting_type, SettingType::Boolean));
656 }
657
658 #[test]
659 fn test_editor_category() {
660 let categories = parse_schema(SAMPLE_SCHEMA).unwrap();
661 let editor = &categories[1];
662
663 assert_eq!(editor.path, "/editor");
664 assert_eq!(editor.settings.len(), 2);
665
666 let tab_size = editor
667 .settings
668 .iter()
669 .find(|s| s.name == "Tab Size")
670 .unwrap();
671 if let SettingType::Integer { minimum, maximum } = &tab_size.setting_type {
672 assert_eq!(*minimum, Some(1));
673 assert_eq!(*maximum, Some(16));
674 } else {
675 panic!("Expected integer type");
676 }
677 }
678
679 #[test]
680 fn test_humanize_name() {
681 assert_eq!(humanize_name("tab_size"), "Tab Size");
682 assert_eq!(humanize_name("line_numbers"), "Line Numbers");
683 assert_eq!(humanize_name("check_for_updates"), "Check For Updates");
684 assert_eq!(humanize_name("lsp"), "Lsp");
685 }
686}