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