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}
77
78#[derive(Debug, Clone)]
80pub enum SettingType {
81 Boolean,
83 Integer {
85 minimum: Option<i64>,
86 maximum: Option<i64>,
87 },
88 Number {
90 minimum: Option<f64>,
91 maximum: Option<f64>,
92 },
93 String,
95 Enum { options: Vec<EnumOption> },
97 StringArray,
99 IntegerArray,
101 ObjectArray {
103 item_schema: Box<SettingSchema>,
104 display_field: Option<String>,
106 },
107 Object { properties: Vec<SettingSchema> },
109 Map {
111 value_schema: Box<SettingSchema>,
112 display_field: Option<String>,
114 no_add: bool,
116 },
117 Complex,
119}
120
121#[derive(Debug, Clone)]
123pub struct EnumOption {
124 pub name: String,
126 pub value: String,
128}
129
130#[derive(Debug, Clone)]
132pub struct SettingCategory {
133 pub name: String,
135 pub path: String,
137 pub description: Option<String>,
139 pub settings: Vec<SettingSchema>,
141 pub subcategories: Vec<SettingCategory>,
143}
144
145#[derive(Debug, Deserialize)]
147struct RawSchema {
148 #[serde(rename = "type")]
149 schema_type: Option<SchemaType>,
150 description: Option<String>,
151 default: Option<serde_json::Value>,
152 properties: Option<HashMap<String, RawSchema>>,
153 items: Option<Box<RawSchema>>,
154 #[serde(rename = "enum")]
155 enum_values: Option<Vec<serde_json::Value>>,
156 minimum: Option<serde_json::Number>,
157 maximum: Option<serde_json::Number>,
158 #[serde(rename = "$ref")]
159 ref_path: Option<String>,
160 #[serde(rename = "$defs")]
161 defs: Option<HashMap<String, RawSchema>>,
162 #[serde(rename = "additionalProperties")]
163 additional_properties: Option<AdditionalProperties>,
164 #[serde(rename = "x-enum-values", default)]
166 extensible_enum_values: Vec<EnumValueEntry>,
167 #[serde(rename = "x-display-field")]
170 display_field: Option<String>,
171 #[serde(rename = "readOnly", default)]
173 read_only: bool,
174 #[serde(rename = "x-standalone-category", default)]
176 standalone_category: bool,
177 #[serde(rename = "x-no-add", default)]
179 no_add: bool,
180 #[serde(rename = "x-section")]
182 section: Option<String>,
183}
184
185#[derive(Debug, Deserialize)]
187struct EnumValueEntry {
188 #[serde(rename = "ref")]
190 ref_path: String,
191 name: Option<String>,
193 value: serde_json::Value,
195}
196
197#[derive(Debug, Deserialize)]
199#[serde(untagged)]
200enum AdditionalProperties {
201 Bool(bool),
202 Schema(Box<RawSchema>),
203}
204
205#[derive(Debug, Deserialize)]
207#[serde(untagged)]
208enum SchemaType {
209 Single(String),
210 Multiple(Vec<String>),
211}
212
213impl SchemaType {
214 fn primary(&self) -> Option<&str> {
216 match self {
217 Self::Single(s) => Some(s.as_str()),
218 Self::Multiple(v) => v.first().map(|s| s.as_str()),
219 }
220 }
221}
222
223type EnumValuesMap = HashMap<String, Vec<EnumOption>>;
225
226pub fn parse_schema(schema_json: &str) -> Result<Vec<SettingCategory>, serde_json::Error> {
228 let raw: RawSchema = serde_json::from_str(schema_json)?;
229
230 let defs = raw.defs.unwrap_or_default();
231 let properties = raw.properties.unwrap_or_default();
232
233 let enum_values_map = build_enum_values_map(&raw.extensible_enum_values);
235
236 let mut categories = Vec::new();
237 let mut top_level_settings = Vec::new();
238
239 let mut sorted_props: Vec<_> = properties.into_iter().collect();
241 sorted_props.sort_by(|a, b| a.0.cmp(&b.0));
242 for (name, prop) in sorted_props {
243 let path = format!("/{}", name);
244 let display_name = humanize_name(&name);
245
246 let resolved = resolve_ref(&prop, &defs);
248
249 if prop.standalone_category {
251 let setting = parse_setting(&name, &path, &prop, &defs, &enum_values_map);
253 categories.push(SettingCategory {
254 name: display_name,
255 path: path.clone(),
256 description: prop.description.clone().or(resolved.description.clone()),
257 settings: vec![setting],
258 subcategories: Vec::new(),
259 });
260 } else if let Some(ref inner_props) = resolved.properties {
261 let settings = parse_properties(inner_props, &path, &defs, &enum_values_map);
263 categories.push(SettingCategory {
264 name: display_name,
265 path: path.clone(),
266 description: resolved.description.clone(),
267 settings,
268 subcategories: Vec::new(),
269 });
270 } else {
271 let setting = parse_setting(&name, &path, &prop, &defs, &enum_values_map);
273 top_level_settings.push(setting);
274 }
275 }
276
277 if !top_level_settings.is_empty() {
279 top_level_settings.sort_by(|a, b| a.name.cmp(&b.name));
281 categories.insert(
282 0,
283 SettingCategory {
284 name: "General".to_string(),
285 path: String::new(),
286 description: Some("General settings".to_string()),
287 settings: top_level_settings,
288 subcategories: Vec::new(),
289 },
290 );
291 }
292
293 categories.sort_by(|a, b| match (a.name.as_str(), b.name.as_str()) {
295 ("General", _) => std::cmp::Ordering::Less,
296 (_, "General") => std::cmp::Ordering::Greater,
297 (a, b) => a.cmp(b),
298 });
299
300 Ok(categories)
301}
302
303fn build_enum_values_map(entries: &[EnumValueEntry]) -> EnumValuesMap {
305 let mut map: EnumValuesMap = HashMap::new();
306
307 for entry in entries {
308 let value_str = match &entry.value {
309 serde_json::Value::String(s) => s.clone(),
310 other => other.to_string(),
311 };
312
313 let option = EnumOption {
314 name: entry.name.clone().unwrap_or_else(|| value_str.clone()),
315 value: value_str,
316 };
317
318 map.entry(entry.ref_path.clone()).or_default().push(option);
319 }
320
321 map
322}
323
324fn parse_properties(
326 properties: &HashMap<String, RawSchema>,
327 parent_path: &str,
328 defs: &HashMap<String, RawSchema>,
329 enum_values_map: &EnumValuesMap,
330) -> Vec<SettingSchema> {
331 let mut settings = Vec::new();
332
333 for (name, prop) in properties {
334 let path = format!("{}/{}", parent_path, name);
335 let setting = parse_setting(name, &path, prop, defs, enum_values_map);
336 settings.push(setting);
337 }
338
339 settings.sort_by(|a, b| a.name.cmp(&b.name));
341
342 settings
343}
344
345fn parse_setting(
347 name: &str,
348 path: &str,
349 schema: &RawSchema,
350 defs: &HashMap<String, RawSchema>,
351 enum_values_map: &EnumValuesMap,
352) -> SettingSchema {
353 let setting_type = determine_type(schema, defs, enum_values_map);
354
355 let resolved = resolve_ref(schema, defs);
357 let description = schema
358 .description
359 .clone()
360 .or_else(|| resolved.description.clone());
361
362 let read_only = schema.read_only || resolved.read_only;
364
365 let section = schema.section.clone().or_else(|| resolved.section.clone());
367
368 SettingSchema {
369 path: path.to_string(),
370 name: i18n_name(path, name),
371 description,
372 setting_type,
373 default: schema.default.clone(),
374 read_only,
375 section,
376 }
377}
378
379fn determine_type(
381 schema: &RawSchema,
382 defs: &HashMap<String, RawSchema>,
383 enum_values_map: &EnumValuesMap,
384) -> SettingType {
385 if let Some(ref ref_path) = schema.ref_path {
387 if let Some(options) = enum_values_map.get(ref_path) {
388 if !options.is_empty() {
389 return SettingType::Enum {
390 options: options.clone(),
391 };
392 }
393 }
394 }
395
396 let resolved = resolve_ref(schema, defs);
398
399 let enum_values = schema
401 .enum_values
402 .as_ref()
403 .or(resolved.enum_values.as_ref());
404 if let Some(values) = enum_values {
405 let options: Vec<EnumOption> = values
406 .iter()
407 .filter_map(|v| {
408 if v.is_null() {
409 Some(EnumOption {
411 name: "Auto-detect".to_string(),
412 value: String::new(), })
414 } else {
415 v.as_str().map(|s| EnumOption {
416 name: s.to_string(),
417 value: s.to_string(),
418 })
419 }
420 })
421 .collect();
422 if !options.is_empty() {
423 return SettingType::Enum { options };
424 }
425 }
426
427 match resolved.schema_type.as_ref().and_then(|t| t.primary()) {
429 Some("boolean") => SettingType::Boolean,
430 Some("integer") => {
431 let minimum = resolved.minimum.as_ref().and_then(|n| n.as_i64());
432 let maximum = resolved.maximum.as_ref().and_then(|n| n.as_i64());
433 SettingType::Integer { minimum, maximum }
434 }
435 Some("number") => {
436 let minimum = resolved.minimum.as_ref().and_then(|n| n.as_f64());
437 let maximum = resolved.maximum.as_ref().and_then(|n| n.as_f64());
438 SettingType::Number { minimum, maximum }
439 }
440 Some("string") => SettingType::String,
441 Some("array") => {
442 if let Some(ref items) = resolved.items {
444 let item_resolved = resolve_ref(items, defs);
445 let item_type = item_resolved.schema_type.as_ref().and_then(|t| t.primary());
446 if item_type == Some("string") {
447 return SettingType::StringArray;
448 }
449 if item_type == Some("integer") || item_type == Some("number") {
450 return SettingType::IntegerArray;
451 }
452 if items.ref_path.is_some() {
454 let item_schema =
456 parse_setting("item", "", item_resolved, defs, enum_values_map);
457
458 if matches!(item_schema.setting_type, SettingType::Object { .. }) {
460 let display_field = item_resolved.display_field.clone();
462 return SettingType::ObjectArray {
463 item_schema: Box::new(item_schema),
464 display_field,
465 };
466 }
467 }
468 }
469 SettingType::Complex
470 }
471 Some("object") => {
472 if let Some(ref add_props) = resolved.additional_properties {
474 match add_props {
475 AdditionalProperties::Schema(schema_box) => {
476 let inner_resolved = resolve_ref(schema_box, defs);
477 let value_schema =
478 parse_setting("value", "", inner_resolved, defs, enum_values_map);
479
480 let display_field = inner_resolved.display_field.clone();
482
483 let no_add = resolved.no_add;
485
486 return SettingType::Map {
487 value_schema: Box::new(value_schema),
488 display_field,
489 no_add,
490 };
491 }
492 AdditionalProperties::Bool(true) => {
493 return SettingType::Complex;
495 }
496 AdditionalProperties::Bool(false) => {
497 }
500 }
501 }
502 if let Some(ref props) = resolved.properties {
504 let properties = parse_properties(props, "", defs, enum_values_map);
505 return SettingType::Object { properties };
506 }
507 SettingType::Complex
508 }
509 _ => SettingType::Complex,
510 }
511}
512
513fn resolve_ref<'a>(schema: &'a RawSchema, defs: &'a HashMap<String, RawSchema>) -> &'a RawSchema {
515 if let Some(ref ref_path) = schema.ref_path {
516 if let Some(def_name) = ref_path.strip_prefix("#/$defs/") {
518 if let Some(def) = defs.get(def_name) {
519 return def;
520 }
521 }
522 }
523 schema
524}
525
526fn i18n_name(path: &str, fallback_name: &str) -> String {
532 let key = format!("settings.field{}", path.replace('/', "."));
533 let translated = t!(&key);
534 if *translated == key {
535 humanize_name(fallback_name)
536 } else {
537 translated.to_string()
538 }
539}
540
541fn humanize_name(name: &str) -> String {
543 name.split('_')
544 .map(|word| {
545 let mut chars = word.chars();
546 match chars.next() {
547 None => String::new(),
548 Some(first) => first.to_uppercase().chain(chars).collect(),
549 }
550 })
551 .collect::<Vec<_>>()
552 .join(" ")
553}
554
555#[cfg(test)]
556mod tests {
557 use super::*;
558
559 const SAMPLE_SCHEMA: &str = r##"
560{
561 "$schema": "https://json-schema.org/draft/2020-12/schema",
562 "title": "Config",
563 "type": "object",
564 "properties": {
565 "theme": {
566 "description": "Color theme name",
567 "type": "string",
568 "default": "high-contrast"
569 },
570 "check_for_updates": {
571 "description": "Check for new versions on quit",
572 "type": "boolean",
573 "default": true
574 },
575 "editor": {
576 "description": "Editor settings",
577 "$ref": "#/$defs/EditorConfig"
578 }
579 },
580 "$defs": {
581 "EditorConfig": {
582 "description": "Editor behavior configuration",
583 "type": "object",
584 "properties": {
585 "tab_size": {
586 "description": "Number of spaces per tab",
587 "type": "integer",
588 "minimum": 1,
589 "maximum": 16,
590 "default": 4
591 },
592 "line_numbers": {
593 "description": "Show line numbers",
594 "type": "boolean",
595 "default": true
596 }
597 }
598 }
599 }
600}
601"##;
602
603 #[test]
604 fn test_parse_schema() {
605 let categories = parse_schema(SAMPLE_SCHEMA).unwrap();
606
607 assert_eq!(categories.len(), 2);
609 assert_eq!(categories[0].name, "General");
610 assert_eq!(categories[1].name, "Editor");
611 }
612
613 #[test]
614 fn test_general_category() {
615 let categories = parse_schema(SAMPLE_SCHEMA).unwrap();
616 let general = &categories[0];
617
618 assert_eq!(general.settings.len(), 2);
620
621 let theme = general
622 .settings
623 .iter()
624 .find(|s| s.path == "/theme")
625 .unwrap();
626 assert!(matches!(theme.setting_type, SettingType::String));
627
628 let updates = general
629 .settings
630 .iter()
631 .find(|s| s.path == "/check_for_updates")
632 .unwrap();
633 assert!(matches!(updates.setting_type, SettingType::Boolean));
634 }
635
636 #[test]
637 fn test_editor_category() {
638 let categories = parse_schema(SAMPLE_SCHEMA).unwrap();
639 let editor = &categories[1];
640
641 assert_eq!(editor.path, "/editor");
642 assert_eq!(editor.settings.len(), 2);
643
644 let tab_size = editor
645 .settings
646 .iter()
647 .find(|s| s.name == "Tab Size")
648 .unwrap();
649 if let SettingType::Integer { minimum, maximum } = &tab_size.setting_type {
650 assert_eq!(*minimum, Some(1));
651 assert_eq!(*maximum, Some(16));
652 } else {
653 panic!("Expected integer type");
654 }
655 }
656
657 #[test]
658 fn test_humanize_name() {
659 assert_eq!(humanize_name("tab_size"), "Tab Size");
660 assert_eq!(humanize_name("line_numbers"), "Line Numbers");
661 assert_eq!(humanize_name("check_for_updates"), "Check For Updates");
662 assert_eq!(humanize_name("lsp"), "Lsp");
663 }
664}