Generalize settings JSON schema logic to work w/ arbitrary setting types

This commit is contained in:
Max Brunsfeld 2023-05-10 09:56:58 -07:00
parent 9a6a2d9d27
commit b6b2c5d1d1
3 changed files with 179 additions and 75 deletions

View file

@ -8,7 +8,7 @@ use gpui::{
fonts, AppContext, AssetSource,
};
use schemars::{
gen::{SchemaGenerator, SchemaSettings},
gen::SchemaGenerator,
schema::{InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec},
JsonSchema,
};
@ -25,7 +25,7 @@ use util::ResultExt as _;
pub use keymap_file::{keymap_file_json_schema, KeymapFileContent};
pub use settings_file::*;
pub use settings_store::SettingsStore;
pub use settings_store::{SettingsJsonSchemaParams, SettingsStore};
pub const DEFAULT_SETTINGS_ASSET_PATH: &str = "settings/default.json";
pub const INITIAL_USER_SETTINGS_ASSET_PATH: &str = "settings/initial_user_settings.json";
@ -150,6 +150,75 @@ impl Setting for Settings {
this
}
fn json_schema(
generator: &mut SchemaGenerator,
params: &SettingsJsonSchemaParams,
) -> schemars::schema::RootSchema {
let mut root_schema = generator.root_schema_for::<SettingsFileContent>();
// Create a schema for a theme name.
let theme_name_schema = SchemaObject {
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
enum_values: Some(
params
.theme_names
.iter()
.cloned()
.map(Value::String)
.collect(),
),
..Default::default()
};
// Create a schema for a 'languages overrides' object, associating editor
// settings with specific langauges.
assert!(root_schema.definitions.contains_key("EditorSettings"));
let languages_object_schema = SchemaObject {
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))),
object: Some(Box::new(ObjectValidation {
properties: params
.language_names
.iter()
.map(|name| {
(
name.clone(),
Schema::new_ref("#/definitions/EditorSettings".into()),
)
})
.collect(),
..Default::default()
})),
..Default::default()
};
// Add these new schemas as definitions, and modify properties of the root
// schema to reference them.
root_schema.definitions.extend([
("ThemeName".into(), theme_name_schema.into()),
("Languages".into(), languages_object_schema.into()),
]);
let root_schema_object = &mut root_schema.schema.object.as_mut().unwrap();
root_schema_object.properties.extend([
(
"theme".to_owned(),
Schema::new_ref("#/definitions/ThemeName".into()),
),
(
"languages".to_owned(),
Schema::new_ref("#/definitions/Languages".into()),
),
// For backward compatibility
(
"language_overrides".to_owned(),
Schema::new_ref("#/definitions/Languages".into()),
),
]);
root_schema
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
@ -926,72 +995,6 @@ impl Settings {
}
}
pub fn settings_file_json_schema(
theme_names: Vec<String>,
language_names: &[String],
) -> serde_json::Value {
let settings = SchemaSettings::draft07().with(|settings| {
settings.option_add_null_type = false;
});
let generator = SchemaGenerator::new(settings);
let mut root_schema = generator.into_root_schema_for::<SettingsFileContent>();
// Create a schema for a theme name.
let theme_name_schema = SchemaObject {
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
enum_values: Some(theme_names.into_iter().map(Value::String).collect()),
..Default::default()
};
// Create a schema for a 'languages overrides' object, associating editor
// settings with specific langauges.
assert!(root_schema.definitions.contains_key("EditorSettings"));
let languages_object_schema = SchemaObject {
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))),
object: Some(Box::new(ObjectValidation {
properties: language_names
.iter()
.map(|name| {
(
name.clone(),
Schema::new_ref("#/definitions/EditorSettings".into()),
)
})
.collect(),
..Default::default()
})),
..Default::default()
};
// Add these new schemas as definitions, and modify properties of the root
// schema to reference them.
root_schema.definitions.extend([
("ThemeName".into(), theme_name_schema.into()),
("Languages".into(), languages_object_schema.into()),
]);
let root_schema_object = &mut root_schema.schema.object.as_mut().unwrap();
root_schema_object.properties.extend([
(
"theme".to_owned(),
Schema::new_ref("#/definitions/ThemeName".into()),
),
(
"languages".to_owned(),
Schema::new_ref("#/definitions/Languages".into()),
),
// For backward compatibility
(
"language_overrides".to_owned(),
Schema::new_ref("#/definitions/Languages".into()),
),
]);
serde_json::to_value(root_schema).unwrap()
}
fn merge<T: Copy>(target: &mut T, value: Option<T>) {
if let Some(value) = value {
*target = value;

View file

@ -1,8 +1,8 @@
use anyhow::{anyhow, Result};
use collections::{hash_map, BTreeMap, HashMap, HashSet};
use collections::{btree_map, hash_map, BTreeMap, HashMap, HashSet};
use gpui::AppContext;
use lazy_static::lazy_static;
use schemars::JsonSchema;
use schemars::{gen::SchemaGenerator, schema::RootSchema, JsonSchema};
use serde::{de::DeserializeOwned, Deserialize as _, Serialize};
use smallvec::SmallVec;
use std::{
@ -39,6 +39,10 @@ pub trait Setting: 'static {
cx: &AppContext,
) -> Self;
fn json_schema(generator: &mut SchemaGenerator, _: &SettingsJsonSchemaParams) -> RootSchema {
generator.root_schema_for::<Self::FileContent>()
}
fn load_via_json_merge(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
@ -54,6 +58,11 @@ pub trait Setting: 'static {
}
}
pub struct SettingsJsonSchemaParams<'a> {
pub theme_names: &'a [String],
pub language_names: &'a [String],
}
/// A set of strongly-typed setting values defined via multiple JSON files.
#[derive(Default)]
pub struct SettingsStore {
@ -84,6 +93,11 @@ trait AnySettingValue {
fn value_for_path(&self, path: Option<&Path>) -> &dyn Any;
fn set_global_value(&mut self, value: Box<dyn Any>);
fn set_local_value(&mut self, path: Arc<Path>, value: Box<dyn Any>);
fn json_schema(
&self,
generator: &mut SchemaGenerator,
_: &SettingsJsonSchemaParams,
) -> RootSchema;
}
struct DeserializedSetting(Box<dyn Any>);
@ -270,6 +284,79 @@ impl SettingsStore {
Ok(())
}
pub fn json_schema(&self, schema_params: &SettingsJsonSchemaParams) -> serde_json::Value {
use schemars::{
gen::SchemaSettings,
schema::{Schema, SchemaObject},
};
let settings = SchemaSettings::draft07().with(|settings| {
settings.option_add_null_type = false;
});
let mut generator = SchemaGenerator::new(settings);
let mut combined_schema = RootSchema::default();
for setting_value in self.setting_values.values() {
let setting_schema = setting_value.json_schema(&mut generator, schema_params);
combined_schema
.definitions
.extend(setting_schema.definitions);
let target_schema = if let Some(key) = setting_value.key() {
let key_schema = combined_schema
.schema
.object()
.properties
.entry(key.to_string())
.or_insert_with(|| Schema::Object(SchemaObject::default()));
if let Schema::Object(key_schema) = key_schema {
key_schema
} else {
continue;
}
} else {
&mut combined_schema.schema
};
merge_schema(target_schema, setting_schema.schema);
}
fn merge_schema(target: &mut SchemaObject, source: SchemaObject) {
if let Some(source) = source.object {
let target_properties = &mut target.object().properties;
for (key, value) in source.properties {
match target_properties.entry(key) {
btree_map::Entry::Vacant(e) => {
e.insert(value);
}
btree_map::Entry::Occupied(e) => {
if let (Schema::Object(target), Schema::Object(src)) =
(e.into_mut(), value)
{
merge_schema(target, src);
}
}
}
}
}
overwrite(&mut target.instance_type, source.instance_type);
overwrite(&mut target.string, source.string);
overwrite(&mut target.number, source.number);
overwrite(&mut target.reference, source.reference);
overwrite(&mut target.array, source.array);
overwrite(&mut target.enum_values, source.enum_values);
fn overwrite<T>(target: &mut Option<T>, source: Option<T>) {
if let Some(source) = source {
*target = Some(source);
}
}
}
serde_json::to_value(&combined_schema).unwrap()
}
fn recompute_values(
&mut self,
user_settings_changed: bool,
@ -457,6 +544,14 @@ impl<T: Setting> AnySettingValue for SettingValue<T> {
Err(ix) => self.local_values.insert(ix, (path, value)),
}
}
fn json_schema(
&self,
generator: &mut SchemaGenerator,
params: &SettingsJsonSchemaParams,
) -> RootSchema {
T::json_schema(generator, params)
}
}
// impl Debug for SettingsStore {

View file

@ -6,7 +6,7 @@ use gpui::AppContext;
use language::{LanguageRegistry, LanguageServerBinary, LanguageServerName, LspAdapter};
use node_runtime::NodeRuntime;
use serde_json::json;
use settings::{keymap_file_json_schema, settings_file_json_schema};
use settings::{keymap_file_json_schema, SettingsJsonSchemaParams, SettingsStore};
use smol::fs;
use staff_mode::StaffMode;
use std::{
@ -128,12 +128,18 @@ impl LspAdapter for JsonLspAdapter {
cx: &mut AppContext,
) -> Option<BoxFuture<'static, serde_json::Value>> {
let action_names = cx.all_action_names().collect::<Vec<_>>();
let theme_names = self
let theme_names = &self
.themes
.list(**cx.default_global::<StaffMode>())
.map(|meta| meta.name)
.collect();
let language_names = self.languages.language_names();
.collect::<Vec<_>>();
let language_names = &self.languages.language_names();
let settings_schema = cx
.global::<SettingsStore>()
.json_schema(&SettingsJsonSchemaParams {
theme_names,
language_names,
});
Some(
future::ready(serde_json::json!({
"json": {
@ -143,7 +149,7 @@ impl LspAdapter for JsonLspAdapter {
"schemas": [
{
"fileMatch": [schema_file_match(&paths::SETTINGS)],
"schema": settings_file_json_schema(theme_names, &language_names),
"schema": settings_schema,
},
{
"fileMatch": [schema_file_match(&paths::KEYMAP)],