1use serde::Deserialize;
56use std::collections::HashMap;
57
58#[derive(Debug, Clone)]
60pub struct SettingSchema {
61 pub path: String,
63 pub name: String,
65 pub description: Option<String>,
67 pub setting_type: SettingType,
69 pub default: Option<serde_json::Value>,
71 pub read_only: bool,
73 pub section: Option<String>,
75}
76
77#[derive(Debug, Clone)]
79pub enum SettingType {
80 Boolean,
82 Integer {
84 minimum: Option<i64>,
85 maximum: Option<i64>,
86 },
87 Number {
89 minimum: Option<f64>,
90 maximum: Option<f64>,
91 },
92 String,
94 Enum { options: Vec<EnumOption> },
96 StringArray,
98 ObjectArray {
100 item_schema: Box<SettingSchema>,
101 display_field: Option<String>,
103 },
104 Object { properties: Vec<SettingSchema> },
106 Map {
108 value_schema: Box<SettingSchema>,
109 display_field: Option<String>,
111 no_add: bool,
113 },
114 Complex,
116}
117
118#[derive(Debug, Clone)]
120pub struct EnumOption {
121 pub name: String,
123 pub value: String,
125}
126
127#[derive(Debug, Clone)]
129pub struct SettingCategory {
130 pub name: String,
132 pub path: String,
134 pub description: Option<String>,
136 pub settings: Vec<SettingSchema>,
138 pub subcategories: Vec<SettingCategory>,
140}
141
142#[derive(Debug, Deserialize)]
144struct RawSchema {
145 #[serde(rename = "type")]
146 schema_type: Option<SchemaType>,
147 description: Option<String>,
148 default: Option<serde_json::Value>,
149 properties: Option<HashMap<String, RawSchema>>,
150 items: Option<Box<RawSchema>>,
151 #[serde(rename = "enum")]
152 enum_values: Option<Vec<serde_json::Value>>,
153 minimum: Option<serde_json::Number>,
154 maximum: Option<serde_json::Number>,
155 #[serde(rename = "$ref")]
156 ref_path: Option<String>,
157 #[serde(rename = "$defs")]
158 defs: Option<HashMap<String, RawSchema>>,
159 #[serde(rename = "additionalProperties")]
160 additional_properties: Option<AdditionalProperties>,
161 #[serde(rename = "x-enum-values", default)]
163 extensible_enum_values: Vec<EnumValueEntry>,
164 #[serde(rename = "x-display-field")]
167 display_field: Option<String>,
168 #[serde(rename = "readOnly", default)]
170 read_only: bool,
171 #[serde(rename = "x-standalone-category", default)]
173 standalone_category: bool,
174 #[serde(rename = "x-no-add", default)]
176 no_add: bool,
177 #[serde(rename = "x-section")]
179 section: Option<String>,
180}
181
182#[derive(Debug, Deserialize)]
184struct EnumValueEntry {
185 #[serde(rename = "ref")]
187 ref_path: String,
188 name: Option<String>,
190 value: serde_json::Value,
192}
193
194#[derive(Debug, Deserialize)]
196#[serde(untagged)]
197enum AdditionalProperties {
198 Bool(bool),
199 Schema(Box<RawSchema>),
200}
201
202#[derive(Debug, Deserialize)]
204#[serde(untagged)]
205enum SchemaType {
206 Single(String),
207 Multiple(Vec<String>),
208}
209
210impl SchemaType {
211 fn primary(&self) -> Option<&str> {
213 match self {
214 Self::Single(s) => Some(s.as_str()),
215 Self::Multiple(v) => v.first().map(|s| s.as_str()),
216 }
217 }
218}
219
220type EnumValuesMap = HashMap<String, Vec<EnumOption>>;
222
223pub fn parse_schema(schema_json: &str) -> Result<Vec<SettingCategory>, serde_json::Error> {
225 let raw: RawSchema = serde_json::from_str(schema_json)?;
226
227 let defs = raw.defs.unwrap_or_default();
228 let properties = raw.properties.unwrap_or_default();
229
230 let enum_values_map = build_enum_values_map(&raw.extensible_enum_values);
232
233 let mut categories = Vec::new();
234 let mut top_level_settings = Vec::new();
235
236 let mut sorted_props: Vec<_> = properties.into_iter().collect();
238 sorted_props.sort_by(|a, b| a.0.cmp(&b.0));
239 for (name, prop) in sorted_props {
240 let path = format!("/{}", name);
241 let display_name = humanize_name(&name);
242
243 let resolved = resolve_ref(&prop, &defs);
245
246 if prop.standalone_category {
248 let setting = parse_setting(&name, &path, &prop, &defs, &enum_values_map);
250 categories.push(SettingCategory {
251 name: display_name,
252 path: path.clone(),
253 description: prop.description.clone().or(resolved.description.clone()),
254 settings: vec![setting],
255 subcategories: Vec::new(),
256 });
257 } else if let Some(ref inner_props) = resolved.properties {
258 let settings = parse_properties(inner_props, &path, &defs, &enum_values_map);
260 categories.push(SettingCategory {
261 name: display_name,
262 path: path.clone(),
263 description: resolved.description.clone(),
264 settings,
265 subcategories: Vec::new(),
266 });
267 } else {
268 let setting = parse_setting(&name, &path, &prop, &defs, &enum_values_map);
270 top_level_settings.push(setting);
271 }
272 }
273
274 if !top_level_settings.is_empty() {
276 top_level_settings.sort_by(|a, b| a.name.cmp(&b.name));
278 categories.insert(
279 0,
280 SettingCategory {
281 name: "General".to_string(),
282 path: String::new(),
283 description: Some("General settings".to_string()),
284 settings: top_level_settings,
285 subcategories: Vec::new(),
286 },
287 );
288 }
289
290 categories.sort_by(|a, b| match (a.name.as_str(), b.name.as_str()) {
292 ("General", _) => std::cmp::Ordering::Less,
293 (_, "General") => std::cmp::Ordering::Greater,
294 (a, b) => a.cmp(b),
295 });
296
297 Ok(categories)
298}
299
300fn build_enum_values_map(entries: &[EnumValueEntry]) -> EnumValuesMap {
302 let mut map: EnumValuesMap = HashMap::new();
303
304 for entry in entries {
305 let value_str = match &entry.value {
306 serde_json::Value::String(s) => s.clone(),
307 other => other.to_string(),
308 };
309
310 let option = EnumOption {
311 name: entry.name.clone().unwrap_or_else(|| value_str.clone()),
312 value: value_str,
313 };
314
315 map.entry(entry.ref_path.clone()).or_default().push(option);
316 }
317
318 map
319}
320
321fn parse_properties(
323 properties: &HashMap<String, RawSchema>,
324 parent_path: &str,
325 defs: &HashMap<String, RawSchema>,
326 enum_values_map: &EnumValuesMap,
327) -> Vec<SettingSchema> {
328 let mut settings = Vec::new();
329
330 for (name, prop) in properties {
331 let path = format!("{}/{}", parent_path, name);
332 let setting = parse_setting(name, &path, prop, defs, enum_values_map);
333 settings.push(setting);
334 }
335
336 settings.sort_by(|a, b| a.name.cmp(&b.name));
338
339 settings
340}
341
342fn parse_setting(
344 name: &str,
345 path: &str,
346 schema: &RawSchema,
347 defs: &HashMap<String, RawSchema>,
348 enum_values_map: &EnumValuesMap,
349) -> SettingSchema {
350 let setting_type = determine_type(schema, defs, enum_values_map);
351
352 let resolved = resolve_ref(schema, defs);
354 let description = schema
355 .description
356 .clone()
357 .or_else(|| resolved.description.clone());
358
359 let read_only = schema.read_only || resolved.read_only;
361
362 let section = schema.section.clone().or_else(|| resolved.section.clone());
364
365 SettingSchema {
366 path: path.to_string(),
367 name: humanize_name(name),
368 description,
369 setting_type,
370 default: schema.default.clone(),
371 read_only,
372 section,
373 }
374}
375
376fn determine_type(
378 schema: &RawSchema,
379 defs: &HashMap<String, RawSchema>,
380 enum_values_map: &EnumValuesMap,
381) -> SettingType {
382 if let Some(ref ref_path) = schema.ref_path {
384 if let Some(options) = enum_values_map.get(ref_path) {
385 if !options.is_empty() {
386 return SettingType::Enum {
387 options: options.clone(),
388 };
389 }
390 }
391 }
392
393 let resolved = resolve_ref(schema, defs);
395
396 let enum_values = schema
398 .enum_values
399 .as_ref()
400 .or(resolved.enum_values.as_ref());
401 if let Some(values) = enum_values {
402 let options: Vec<EnumOption> = values
403 .iter()
404 .filter_map(|v| {
405 if v.is_null() {
406 Some(EnumOption {
408 name: "Auto-detect".to_string(),
409 value: String::new(), })
411 } else {
412 v.as_str().map(|s| EnumOption {
413 name: s.to_string(),
414 value: s.to_string(),
415 })
416 }
417 })
418 .collect();
419 if !options.is_empty() {
420 return SettingType::Enum { options };
421 }
422 }
423
424 match resolved.schema_type.as_ref().and_then(|t| t.primary()) {
426 Some("boolean") => SettingType::Boolean,
427 Some("integer") => {
428 let minimum = resolved.minimum.as_ref().and_then(|n| n.as_i64());
429 let maximum = resolved.maximum.as_ref().and_then(|n| n.as_i64());
430 SettingType::Integer { minimum, maximum }
431 }
432 Some("number") => {
433 let minimum = resolved.minimum.as_ref().and_then(|n| n.as_f64());
434 let maximum = resolved.maximum.as_ref().and_then(|n| n.as_f64());
435 SettingType::Number { minimum, maximum }
436 }
437 Some("string") => SettingType::String,
438 Some("array") => {
439 if let Some(ref items) = resolved.items {
441 let item_resolved = resolve_ref(items, defs);
442 if item_resolved.schema_type.as_ref().and_then(|t| t.primary()) == Some("string") {
443 return SettingType::StringArray;
444 }
445 if items.ref_path.is_some() {
447 let item_schema =
449 parse_setting("item", "", item_resolved, defs, enum_values_map);
450
451 if matches!(item_schema.setting_type, SettingType::Object { .. }) {
453 let display_field = item_resolved.display_field.clone();
455 return SettingType::ObjectArray {
456 item_schema: Box::new(item_schema),
457 display_field,
458 };
459 }
460 }
461 }
462 SettingType::Complex
463 }
464 Some("object") => {
465 if let Some(ref add_props) = resolved.additional_properties {
467 match add_props {
468 AdditionalProperties::Schema(schema_box) => {
469 let inner_resolved = resolve_ref(schema_box, defs);
470 let value_schema =
471 parse_setting("value", "", inner_resolved, defs, enum_values_map);
472
473 let display_field = inner_resolved.display_field.clone();
475
476 let no_add = resolved.no_add;
478
479 return SettingType::Map {
480 value_schema: Box::new(value_schema),
481 display_field,
482 no_add,
483 };
484 }
485 AdditionalProperties::Bool(true) => {
486 return SettingType::Complex;
488 }
489 AdditionalProperties::Bool(false) => {
490 }
493 }
494 }
495 if let Some(ref props) = resolved.properties {
497 let properties = parse_properties(props, "", defs, enum_values_map);
498 return SettingType::Object { properties };
499 }
500 SettingType::Complex
501 }
502 _ => SettingType::Complex,
503 }
504}
505
506fn resolve_ref<'a>(schema: &'a RawSchema, defs: &'a HashMap<String, RawSchema>) -> &'a RawSchema {
508 if let Some(ref ref_path) = schema.ref_path {
509 if let Some(def_name) = ref_path.strip_prefix("#/$defs/") {
511 if let Some(def) = defs.get(def_name) {
512 return def;
513 }
514 }
515 }
516 schema
517}
518
519fn humanize_name(name: &str) -> String {
521 name.split('_')
522 .map(|word| {
523 let mut chars = word.chars();
524 match chars.next() {
525 None => String::new(),
526 Some(first) => first.to_uppercase().chain(chars).collect(),
527 }
528 })
529 .collect::<Vec<_>>()
530 .join(" ")
531}
532
533#[cfg(test)]
534mod tests {
535 use super::*;
536
537 const SAMPLE_SCHEMA: &str = r##"
538{
539 "$schema": "https://json-schema.org/draft/2020-12/schema",
540 "title": "Config",
541 "type": "object",
542 "properties": {
543 "theme": {
544 "description": "Color theme name",
545 "type": "string",
546 "default": "high-contrast"
547 },
548 "check_for_updates": {
549 "description": "Check for new versions on quit",
550 "type": "boolean",
551 "default": true
552 },
553 "editor": {
554 "description": "Editor settings",
555 "$ref": "#/$defs/EditorConfig"
556 }
557 },
558 "$defs": {
559 "EditorConfig": {
560 "description": "Editor behavior configuration",
561 "type": "object",
562 "properties": {
563 "tab_size": {
564 "description": "Number of spaces per tab",
565 "type": "integer",
566 "minimum": 1,
567 "maximum": 16,
568 "default": 4
569 },
570 "line_numbers": {
571 "description": "Show line numbers",
572 "type": "boolean",
573 "default": true
574 }
575 }
576 }
577 }
578}
579"##;
580
581 #[test]
582 fn test_parse_schema() {
583 let categories = parse_schema(SAMPLE_SCHEMA).unwrap();
584
585 assert_eq!(categories.len(), 2);
587 assert_eq!(categories[0].name, "General");
588 assert_eq!(categories[1].name, "Editor");
589 }
590
591 #[test]
592 fn test_general_category() {
593 let categories = parse_schema(SAMPLE_SCHEMA).unwrap();
594 let general = &categories[0];
595
596 assert_eq!(general.settings.len(), 2);
598
599 let theme = general
600 .settings
601 .iter()
602 .find(|s| s.path == "/theme")
603 .unwrap();
604 assert!(matches!(theme.setting_type, SettingType::String));
605
606 let updates = general
607 .settings
608 .iter()
609 .find(|s| s.path == "/check_for_updates")
610 .unwrap();
611 assert!(matches!(updates.setting_type, SettingType::Boolean));
612 }
613
614 #[test]
615 fn test_editor_category() {
616 let categories = parse_schema(SAMPLE_SCHEMA).unwrap();
617 let editor = &categories[1];
618
619 assert_eq!(editor.path, "/editor");
620 assert_eq!(editor.settings.len(), 2);
621
622 let tab_size = editor
623 .settings
624 .iter()
625 .find(|s| s.name == "Tab Size")
626 .unwrap();
627 if let SettingType::Integer { minimum, maximum } = &tab_size.setting_type {
628 assert_eq!(*minimum, Some(1));
629 assert_eq!(*maximum, Some(16));
630 } else {
631 panic!("Expected integer type");
632 }
633 }
634
635 #[test]
636 fn test_humanize_name() {
637 assert_eq!(humanize_name("tab_size"), "Tab Size");
638 assert_eq!(humanize_name("line_numbers"), "Line Numbers");
639 assert_eq!(humanize_name("check_for_updates"), "Check For Updates");
640 assert_eq!(humanize_name("lsp"), "Lsp");
641 }
642}