Migrate to schemars version 1.0 (#33635)
The major change in schemars 1.0 is that now schemas are represented as plain json values instead of specialized datatypes. This allows for more concise construction and manipulation. This change also improves how settings schemas are generated. Each top level settings type was being generated as a full root schema including the definitions it references, and then these were merged. This meant generating all shared definitions multiple times, and might have bugs in cases where there are two types with the same names. Now instead the schemar generator's `definitions` are built up as they normally are and the `Settings` trait no longer has a special `json_schema` method. To handle types that have schema that vary at runtime (`FontFamilyName`, `ThemeName`, etc), values of `ParameterizedJsonSchema` are collected by `inventory`, and the schema definitions for these types are replaced. To help check that this doesn't break anything, I tried to minimize the overall [schema diff](https://gist.github.com/mgsloan/1de549def20399d6f37943a3c1583ee7) with some patches to make the order more consistent + schemas also sorted with `jq -S .`. A skim of the diff shows that the diffs come from: * `enum: ["value"]` turning into `const: "value"` * Differences in handling of newlines for "description" * Schemas for generic types no longer including the parameter name, now all disambiguation is with numeric suffixes * Enums now using `oneOf` instead of `anyOf`. Release Notes: - N/A
This commit is contained in:
parent
a2e786e0f9
commit
5fafab6e52
42 changed files with 714 additions and 963 deletions
|
@ -5,13 +5,10 @@ use gpui::{
|
|||
Action, ActionBuildError, App, InvalidKeystrokeError, KEYSTROKE_PARSE_EXPECTED_MESSAGE,
|
||||
KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, NoAction,
|
||||
};
|
||||
use schemars::{
|
||||
JsonSchema,
|
||||
r#gen::{SchemaGenerator, SchemaSettings},
|
||||
schema::{ArrayValidation, InstanceType, Schema, SchemaObject, SubschemaValidation},
|
||||
};
|
||||
use schemars::{JsonSchema, json_schema};
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use serde_json::{Value, json};
|
||||
use std::borrow::Cow;
|
||||
use std::{any::TypeId, fmt::Write, rc::Rc, sync::Arc, sync::LazyLock};
|
||||
use util::{
|
||||
asset_str,
|
||||
|
@ -123,14 +120,14 @@ impl std::fmt::Display for KeymapAction {
|
|||
impl JsonSchema for KeymapAction {
|
||||
/// This is used when generating the JSON schema for the `KeymapAction` type, so that it can
|
||||
/// reference the keymap action schema.
|
||||
fn schema_name() -> String {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"KeymapAction".into()
|
||||
}
|
||||
|
||||
/// This schema will be replaced with the full action schema in
|
||||
/// `KeymapFile::generate_json_schema`.
|
||||
fn json_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
Schema::Bool(true)
|
||||
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
json_schema!(true)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -424,9 +421,7 @@ impl KeymapFile {
|
|||
}
|
||||
|
||||
pub fn generate_json_schema_for_registered_actions(cx: &mut App) -> Value {
|
||||
let mut generator = SchemaSettings::draft07()
|
||||
.with(|settings| settings.option_add_null_type = false)
|
||||
.into_generator();
|
||||
let mut generator = schemars::generate::SchemaSettings::draft07().into_generator();
|
||||
|
||||
let action_schemas = cx.action_schemas(&mut generator);
|
||||
let deprecations = cx.deprecated_actions_to_preferred_actions();
|
||||
|
@ -440,92 +435,70 @@ impl KeymapFile {
|
|||
}
|
||||
|
||||
fn generate_json_schema(
|
||||
generator: SchemaGenerator,
|
||||
action_schemas: Vec<(&'static str, Option<Schema>)>,
|
||||
mut generator: schemars::SchemaGenerator,
|
||||
action_schemas: Vec<(&'static str, Option<schemars::Schema>)>,
|
||||
deprecations: &HashMap<&'static str, &'static str>,
|
||||
deprecation_messages: &HashMap<&'static str, &'static str>,
|
||||
) -> serde_json::Value {
|
||||
fn set<I, O>(input: I) -> Option<O>
|
||||
where
|
||||
I: Into<O>,
|
||||
{
|
||||
Some(input.into())
|
||||
}
|
||||
|
||||
fn add_deprecation(schema_object: &mut SchemaObject, message: String) {
|
||||
schema_object.extensions.insert(
|
||||
// deprecationMessage is not part of the JSON Schema spec,
|
||||
// but json-language-server recognizes it.
|
||||
"deprecationMessage".to_owned(),
|
||||
fn add_deprecation(schema: &mut schemars::Schema, message: String) {
|
||||
schema.insert(
|
||||
// deprecationMessage is not part of the JSON Schema spec, but
|
||||
// json-language-server recognizes it.
|
||||
"deprecationMessage".to_string(),
|
||||
Value::String(message),
|
||||
);
|
||||
}
|
||||
|
||||
fn add_deprecation_preferred_name(schema_object: &mut SchemaObject, new_name: &str) {
|
||||
add_deprecation(schema_object, format!("Deprecated, use {new_name}"));
|
||||
fn add_deprecation_preferred_name(schema: &mut schemars::Schema, new_name: &str) {
|
||||
add_deprecation(schema, format!("Deprecated, use {new_name}"));
|
||||
}
|
||||
|
||||
fn add_description(schema_object: &mut SchemaObject, description: String) {
|
||||
schema_object
|
||||
.metadata
|
||||
.get_or_insert(Default::default())
|
||||
.description = Some(description);
|
||||
fn add_description(schema: &mut schemars::Schema, description: String) {
|
||||
schema.insert("description".to_string(), Value::String(description));
|
||||
}
|
||||
|
||||
let empty_object: SchemaObject = SchemaObject {
|
||||
instance_type: set(InstanceType::Object),
|
||||
..Default::default()
|
||||
};
|
||||
let empty_object = json_schema!({
|
||||
"type": "object"
|
||||
});
|
||||
|
||||
// This is a workaround for a json-language-server issue where it matches the first
|
||||
// alternative that matches the value's shape and uses that for documentation.
|
||||
//
|
||||
// In the case of the array validations, it would even provide an error saying that the name
|
||||
// must match the name of the first alternative.
|
||||
let mut plain_action = SchemaObject {
|
||||
instance_type: set(InstanceType::String),
|
||||
const_value: Some(Value::String("".to_owned())),
|
||||
..Default::default()
|
||||
};
|
||||
let mut plain_action = json_schema!({
|
||||
"type": "string",
|
||||
"const": ""
|
||||
});
|
||||
let no_action_message = "No action named this.";
|
||||
add_description(&mut plain_action, no_action_message.to_owned());
|
||||
add_deprecation(&mut plain_action, no_action_message.to_owned());
|
||||
let mut matches_action_name = SchemaObject {
|
||||
const_value: Some(Value::String("".to_owned())),
|
||||
..Default::default()
|
||||
};
|
||||
let no_action_message = "No action named this that takes input.";
|
||||
add_description(&mut matches_action_name, no_action_message.to_owned());
|
||||
add_deprecation(&mut matches_action_name, no_action_message.to_owned());
|
||||
let action_with_input = SchemaObject {
|
||||
instance_type: set(InstanceType::Array),
|
||||
array: set(ArrayValidation {
|
||||
items: set(vec![
|
||||
matches_action_name.into(),
|
||||
// Accept any value, as we want this to be the preferred match when there is a
|
||||
// typo in the name.
|
||||
Schema::Bool(true),
|
||||
]),
|
||||
min_items: Some(2),
|
||||
max_items: Some(2),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let mut keymap_action_alternatives = vec![plain_action.into(), action_with_input.into()];
|
||||
|
||||
let mut matches_action_name = json_schema!({
|
||||
"const": ""
|
||||
});
|
||||
let no_action_message_input = "No action named this that takes input.";
|
||||
add_description(&mut matches_action_name, no_action_message_input.to_owned());
|
||||
add_deprecation(&mut matches_action_name, no_action_message_input.to_owned());
|
||||
|
||||
let action_with_input = json_schema!({
|
||||
"type": "array",
|
||||
"items": [
|
||||
matches_action_name,
|
||||
true
|
||||
],
|
||||
"minItems": 2,
|
||||
"maxItems": 2
|
||||
});
|
||||
let mut keymap_action_alternatives = vec![plain_action, action_with_input];
|
||||
|
||||
for (name, action_schema) in action_schemas.into_iter() {
|
||||
let schema = if let Some(Schema::Object(schema)) = action_schema {
|
||||
Some(schema)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let description = schema.as_ref().and_then(|schema| {
|
||||
let description = action_schema.as_ref().and_then(|schema| {
|
||||
schema
|
||||
.metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| metadata.description.clone())
|
||||
.as_object()
|
||||
.and_then(|obj| obj.get("description"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
});
|
||||
|
||||
let deprecation = if name == NoAction.name() {
|
||||
|
@ -535,84 +508,64 @@ impl KeymapFile {
|
|||
};
|
||||
|
||||
// Add an alternative for plain action names.
|
||||
let mut plain_action = SchemaObject {
|
||||
instance_type: set(InstanceType::String),
|
||||
const_value: Some(Value::String(name.to_string())),
|
||||
..Default::default()
|
||||
};
|
||||
let mut plain_action = json_schema!({
|
||||
"type": "string",
|
||||
"const": name
|
||||
});
|
||||
if let Some(message) = deprecation_messages.get(name) {
|
||||
add_deprecation(&mut plain_action, message.to_string());
|
||||
} else if let Some(new_name) = deprecation {
|
||||
add_deprecation_preferred_name(&mut plain_action, new_name);
|
||||
}
|
||||
if let Some(description) = description.clone() {
|
||||
add_description(&mut plain_action, description);
|
||||
if let Some(desc) = description.clone() {
|
||||
add_description(&mut plain_action, desc);
|
||||
}
|
||||
keymap_action_alternatives.push(plain_action.into());
|
||||
keymap_action_alternatives.push(plain_action);
|
||||
|
||||
// Add an alternative for actions with data specified as a [name, data] array.
|
||||
//
|
||||
// When a struct with no deserializable fields is added with impl_actions! /
|
||||
// impl_actions_as! an empty object schema is produced. The action should be invoked
|
||||
// without data in this case.
|
||||
if let Some(schema) = schema {
|
||||
// When a struct with no deserializable fields is added by deriving `Action`, an empty
|
||||
// object schema is produced. The action should be invoked without data in this case.
|
||||
if let Some(schema) = action_schema {
|
||||
if schema != empty_object {
|
||||
let mut matches_action_name = SchemaObject {
|
||||
const_value: Some(Value::String(name.to_string())),
|
||||
..Default::default()
|
||||
};
|
||||
if let Some(description) = description.clone() {
|
||||
add_description(&mut matches_action_name, description);
|
||||
let mut matches_action_name = json_schema!({
|
||||
"const": name
|
||||
});
|
||||
if let Some(desc) = description.clone() {
|
||||
add_description(&mut matches_action_name, desc);
|
||||
}
|
||||
if let Some(message) = deprecation_messages.get(name) {
|
||||
add_deprecation(&mut matches_action_name, message.to_string());
|
||||
} else if let Some(new_name) = deprecation {
|
||||
add_deprecation_preferred_name(&mut matches_action_name, new_name);
|
||||
}
|
||||
let action_with_input = SchemaObject {
|
||||
instance_type: set(InstanceType::Array),
|
||||
array: set(ArrayValidation {
|
||||
items: set(vec![matches_action_name.into(), schema.into()]),
|
||||
min_items: Some(2),
|
||||
max_items: Some(2),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
keymap_action_alternatives.push(action_with_input.into());
|
||||
let action_with_input = json_schema!({
|
||||
"type": "array",
|
||||
"items": [matches_action_name, schema],
|
||||
"minItems": 2,
|
||||
"maxItems": 2
|
||||
});
|
||||
keymap_action_alternatives.push(action_with_input);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Placing null first causes json-language-server to default assuming actions should be
|
||||
// null, so place it last.
|
||||
keymap_action_alternatives.push(
|
||||
SchemaObject {
|
||||
instance_type: set(InstanceType::Null),
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
keymap_action_alternatives.push(json_schema!({
|
||||
"type": "null"
|
||||
}));
|
||||
|
||||
// The `KeymapSection` schema will reference the `KeymapAction` schema by name, so setting
|
||||
// the definition of `KeymapAction` results in the full action schema being used.
|
||||
generator.definitions_mut().insert(
|
||||
KeymapAction::schema_name().to_string(),
|
||||
json!({
|
||||
"oneOf": keymap_action_alternatives
|
||||
}),
|
||||
);
|
||||
|
||||
let action_schema = SchemaObject {
|
||||
subschemas: set(SubschemaValidation {
|
||||
one_of: Some(keymap_action_alternatives),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
.into();
|
||||
|
||||
// The `KeymapSection` schema will reference the `KeymapAction` schema by name, so replacing
|
||||
// the definition of `KeymapAction` results in the full action schema being used.
|
||||
let mut root_schema = generator.into_root_schema_for::<KeymapFile>();
|
||||
root_schema
|
||||
.definitions
|
||||
.insert(KeymapAction::schema_name(), action_schema);
|
||||
|
||||
// This and other json schemas can be viewed via `dev: open language server logs` ->
|
||||
// `json-language-server` -> `Server Info`.
|
||||
serde_json::to_value(root_schema).unwrap()
|
||||
generator.root_schema_for::<KeymapFile>().to_value()
|
||||
}
|
||||
|
||||
pub fn sections(&self) -> impl DoubleEndedIterator<Item = &KeymapSection> {
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
use std::{ops::Range, sync::LazyLock};
|
||||
|
||||
use anyhow::Result;
|
||||
use schemars::schema::{
|
||||
ArrayValidation, InstanceType, RootSchema, Schema, SchemaObject, SingleOrVec,
|
||||
};
|
||||
use gpui::App;
|
||||
use schemars::{JsonSchema, Schema};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use serde_json::Value;
|
||||
use std::{ops::Range, sync::LazyLock};
|
||||
use tree_sitter::{Query, StreamingIterator as _};
|
||||
use util::RangeExt;
|
||||
|
||||
|
@ -14,70 +12,43 @@ pub struct SettingsJsonSchemaParams<'a> {
|
|||
pub font_names: &'a [String],
|
||||
}
|
||||
|
||||
impl SettingsJsonSchemaParams<'_> {
|
||||
pub fn font_family_schema(&self) -> Schema {
|
||||
let available_fonts: Vec<_> = self.font_names.iter().cloned().map(Value::String).collect();
|
||||
|
||||
SchemaObject {
|
||||
instance_type: Some(InstanceType::String.into()),
|
||||
enum_values: Some(available_fonts),
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn font_fallback_schema(&self) -> Schema {
|
||||
SchemaObject {
|
||||
instance_type: Some(SingleOrVec::Vec(vec![
|
||||
InstanceType::Array,
|
||||
InstanceType::Null,
|
||||
])),
|
||||
array: Some(Box::new(ArrayValidation {
|
||||
items: Some(schemars::schema::SingleOrVec::Single(Box::new(
|
||||
self.font_family_schema(),
|
||||
))),
|
||||
unique_items: Some(true),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
}
|
||||
pub struct ParameterizedJsonSchema {
|
||||
pub add_and_get_ref:
|
||||
fn(&mut schemars::SchemaGenerator, &SettingsJsonSchemaParams, &App) -> schemars::Schema,
|
||||
}
|
||||
|
||||
type PropertyName<'a> = &'a str;
|
||||
type ReferencePath<'a> = &'a str;
|
||||
inventory::collect!(ParameterizedJsonSchema);
|
||||
|
||||
/// Modifies the provided [`RootSchema`] by adding references to all of the specified properties.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # let root_schema = RootSchema::default();
|
||||
/// add_references_to_properties(&mut root_schema, &[
|
||||
/// ("property_a", "#/definitions/DefinitionA"),
|
||||
/// ("property_b", "#/definitions/DefinitionB"),
|
||||
/// ])
|
||||
/// ```
|
||||
pub fn add_references_to_properties(
|
||||
root_schema: &mut RootSchema,
|
||||
properties_with_references: &[(PropertyName, ReferencePath)],
|
||||
) {
|
||||
for (property, definition) in properties_with_references {
|
||||
let Some(schema) = root_schema.schema.object().properties.get_mut(*property) else {
|
||||
log::warn!("property '{property}' not found in JSON schema");
|
||||
continue;
|
||||
};
|
||||
|
||||
match schema {
|
||||
Schema::Object(schema) => {
|
||||
schema.reference = Some(definition.to_string());
|
||||
}
|
||||
Schema::Bool(_) => {
|
||||
// Boolean schemas can't have references.
|
||||
}
|
||||
pub fn replace_subschema<T: JsonSchema>(
|
||||
generator: &mut schemars::SchemaGenerator,
|
||||
schema: schemars::Schema,
|
||||
) -> schemars::Schema {
|
||||
const DEFINITIONS_PATH: &str = "#/definitions/";
|
||||
// The key in definitions may not match T::schema_name() if multiple types have the same name.
|
||||
// This is a workaround for there being no straightforward way to get the key used for a type -
|
||||
// see https://github.com/GREsau/schemars/issues/449
|
||||
let ref_schema = generator.subschema_for::<T>();
|
||||
if let Some(serde_json::Value::String(definition_pointer)) = ref_schema.get("$ref") {
|
||||
if let Some(definition_name) = definition_pointer.strip_prefix(DEFINITIONS_PATH) {
|
||||
generator
|
||||
.definitions_mut()
|
||||
.insert(definition_name.to_string(), schema.to_value());
|
||||
return ref_schema;
|
||||
} else {
|
||||
log::error!(
|
||||
"bug: expected `$ref` field to start with {DEFINITIONS_PATH}, \
|
||||
got {definition_pointer}"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
log::error!("bug: expected `$ref` field in result of `subschema_for`");
|
||||
}
|
||||
// fallback on just using the schema name, which could collide.
|
||||
let schema_name = T::schema_name();
|
||||
generator
|
||||
.definitions_mut()
|
||||
.insert(schema_name.to_string(), schema.to_value());
|
||||
Schema::new_ref(format!("{DEFINITIONS_PATH}{schema_name}"))
|
||||
}
|
||||
|
||||
pub fn update_value_in_json_text<'a>(
|
||||
|
|
|
@ -6,7 +6,7 @@ use futures::{FutureExt, StreamExt, channel::mpsc, future::LocalBoxFuture};
|
|||
use gpui::{App, AsyncApp, BorrowAppContext, Global, Task, UpdateGlobal};
|
||||
|
||||
use paths::{EDITORCONFIG_NAME, local_settings_file_relative_path, task_file_name};
|
||||
use schemars::{JsonSchema, r#gen::SchemaGenerator, schema::RootSchema};
|
||||
use schemars::{JsonSchema, json_schema};
|
||||
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||
use serde_json::{Value, json};
|
||||
use smallvec::SmallVec;
|
||||
|
@ -24,8 +24,8 @@ use util::{ResultExt as _, merge_non_null_json_value_into};
|
|||
pub type EditorconfigProperties = ec4rs::Properties;
|
||||
|
||||
use crate::{
|
||||
SettingsJsonSchemaParams, VsCodeSettings, WorktreeId, parse_json_with_comments,
|
||||
update_value_in_json_text,
|
||||
ParameterizedJsonSchema, SettingsJsonSchemaParams, VsCodeSettings, WorktreeId,
|
||||
parse_json_with_comments, update_value_in_json_text,
|
||||
};
|
||||
|
||||
/// A value that can be defined as a user setting.
|
||||
|
@ -57,14 +57,6 @@ pub trait Settings: 'static + Send + Sync {
|
|||
where
|
||||
Self: Sized;
|
||||
|
||||
fn json_schema(
|
||||
generator: &mut SchemaGenerator,
|
||||
_: &SettingsJsonSchemaParams,
|
||||
_: &App,
|
||||
) -> RootSchema {
|
||||
generator.root_schema_for::<Self::FileContent>()
|
||||
}
|
||||
|
||||
fn missing_default() -> anyhow::Error {
|
||||
anyhow::anyhow!("missing default")
|
||||
}
|
||||
|
@ -253,12 +245,7 @@ trait AnySettingValue: 'static + Send + Sync {
|
|||
fn all_local_values(&self) -> Vec<(WorktreeId, Arc<Path>, &dyn Any)>;
|
||||
fn set_global_value(&mut self, value: Box<dyn Any>);
|
||||
fn set_local_value(&mut self, root_id: WorktreeId, path: Arc<Path>, value: Box<dyn Any>);
|
||||
fn json_schema(
|
||||
&self,
|
||||
generator: &mut SchemaGenerator,
|
||||
_: &SettingsJsonSchemaParams,
|
||||
cx: &App,
|
||||
) -> RootSchema;
|
||||
fn json_schema(&self, generator: &mut schemars::SchemaGenerator) -> schemars::Schema;
|
||||
fn edits_for_update(
|
||||
&self,
|
||||
raw_settings: &serde_json::Value,
|
||||
|
@ -276,11 +263,11 @@ impl SettingsStore {
|
|||
let (setting_file_updates_tx, mut setting_file_updates_rx) = mpsc::unbounded();
|
||||
Self {
|
||||
setting_values: Default::default(),
|
||||
raw_default_settings: serde_json::json!({}),
|
||||
raw_default_settings: json!({}),
|
||||
raw_global_settings: None,
|
||||
raw_user_settings: serde_json::json!({}),
|
||||
raw_user_settings: json!({}),
|
||||
raw_server_settings: None,
|
||||
raw_extension_settings: serde_json::json!({}),
|
||||
raw_extension_settings: json!({}),
|
||||
raw_local_settings: Default::default(),
|
||||
raw_editorconfig_settings: BTreeMap::default(),
|
||||
tab_size_callback: Default::default(),
|
||||
|
@ -877,108 +864,151 @@ impl SettingsStore {
|
|||
}
|
||||
|
||||
pub fn json_schema(&self, schema_params: &SettingsJsonSchemaParams, cx: &App) -> Value {
|
||||
use schemars::{
|
||||
r#gen::SchemaSettings,
|
||||
schema::{Schema, SchemaObject},
|
||||
};
|
||||
|
||||
let settings = SchemaSettings::draft07().with(|settings| {
|
||||
settings.option_add_null_type = true;
|
||||
let mut generator = schemars::generate::SchemaSettings::draft07().into_generator();
|
||||
let mut combined_schema = json!({
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
});
|
||||
let mut generator = SchemaGenerator::new(settings);
|
||||
let mut combined_schema = RootSchema::default();
|
||||
|
||||
// Merge together settings schemas, similarly to json schema's "allOf". This merging is
|
||||
// recursive, though at time of writing this recursive nature isn't used very much. An
|
||||
// example of it is the schema for `jupyter` having contribution from both `EditorSettings`
|
||||
// and `JupyterSettings`.
|
||||
//
|
||||
// This logic could be removed in favor of "allOf", but then there isn't the opportunity to
|
||||
// validate and fully control the merge.
|
||||
for setting_value in self.setting_values.values() {
|
||||
let setting_schema = setting_value.json_schema(&mut generator, schema_params, cx);
|
||||
combined_schema
|
||||
.definitions
|
||||
.extend(setting_schema.definitions);
|
||||
let mut setting_schema = setting_value.json_schema(&mut generator);
|
||||
|
||||
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, mut source: SchemaObject) {
|
||||
let source_subschemas = source.subschemas();
|
||||
let target_subschemas = target.subschemas();
|
||||
if let Some(all_of) = source_subschemas.all_of.take() {
|
||||
target_subschemas
|
||||
.all_of
|
||||
.get_or_insert(Vec::new())
|
||||
.extend(all_of);
|
||||
}
|
||||
if let Some(any_of) = source_subschemas.any_of.take() {
|
||||
target_subschemas
|
||||
.any_of
|
||||
.get_or_insert(Vec::new())
|
||||
.extend(any_of);
|
||||
}
|
||||
if let Some(one_of) = source_subschemas.one_of.take() {
|
||||
target_subschemas
|
||||
.one_of
|
||||
.get_or_insert(Vec::new())
|
||||
.extend(one_of);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
if let Some(key) = setting_value.key() {
|
||||
if let Some(properties) = combined_schema.get_mut("properties") {
|
||||
if let Some(properties_obj) = properties.as_object_mut() {
|
||||
if let Some(target) = properties_obj.get_mut(key) {
|
||||
merge_schema(target, setting_schema.to_value());
|
||||
} else {
|
||||
properties_obj.insert(key.to_string(), setting_schema.to_value());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setting_schema.remove("description");
|
||||
setting_schema.remove("additionalProperties");
|
||||
merge_schema(&mut combined_schema, setting_schema.to_value());
|
||||
}
|
||||
}
|
||||
|
||||
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 merge_schema(target: &mut serde_json::Value, source: serde_json::Value) {
|
||||
let (Some(target_obj), serde_json::Value::Object(source_obj)) =
|
||||
(target.as_object_mut(), source)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
fn overwrite<T>(target: &mut Option<T>, source: Option<T>) {
|
||||
if let Some(source) = source {
|
||||
*target = Some(source);
|
||||
for (source_key, source_value) in source_obj {
|
||||
match source_key.as_str() {
|
||||
"properties" => {
|
||||
let serde_json::Value::Object(source_properties) = source_value else {
|
||||
log::error!(
|
||||
"bug: expected object for `{}` json schema field, but got: {}",
|
||||
source_key,
|
||||
source_value
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let target_properties =
|
||||
target_obj.entry(source_key.clone()).or_insert(json!({}));
|
||||
let Some(target_properties) = target_properties.as_object_mut() else {
|
||||
log::error!(
|
||||
"bug: expected object for `{}` json schema field, but got: {}",
|
||||
source_key,
|
||||
target_properties
|
||||
);
|
||||
continue;
|
||||
};
|
||||
for (key, value) in source_properties {
|
||||
if let Some(existing) = target_properties.get_mut(&key) {
|
||||
merge_schema(existing, value);
|
||||
} else {
|
||||
target_properties.insert(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
"allOf" | "anyOf" | "oneOf" => {
|
||||
let serde_json::Value::Array(source_array) = source_value else {
|
||||
log::error!(
|
||||
"bug: expected array for `{}` json schema field, but got: {}",
|
||||
source_key,
|
||||
source_value,
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let target_array =
|
||||
target_obj.entry(source_key.clone()).or_insert(json!([]));
|
||||
let Some(target_array) = target_array.as_array_mut() else {
|
||||
log::error!(
|
||||
"bug: expected array for `{}` json schema field, but got: {}",
|
||||
source_key,
|
||||
target_array,
|
||||
);
|
||||
continue;
|
||||
};
|
||||
target_array.extend(source_array);
|
||||
}
|
||||
"type"
|
||||
| "$ref"
|
||||
| "enum"
|
||||
| "minimum"
|
||||
| "maximum"
|
||||
| "pattern"
|
||||
| "description"
|
||||
| "additionalProperties" => {
|
||||
if let Some(old_value) =
|
||||
target_obj.insert(source_key.clone(), source_value.clone())
|
||||
{
|
||||
if old_value != source_value {
|
||||
log::error!(
|
||||
"bug: while merging JSON schemas, \
|
||||
mismatch `\"{}\": {}` (before was `{}`)",
|
||||
source_key,
|
||||
old_value,
|
||||
source_value
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
log::error!(
|
||||
"bug: while merging settings JSON schemas, \
|
||||
encountered unexpected `\"{}\": {}`",
|
||||
source_key,
|
||||
source_value
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for parameterized_json_schema in inventory::iter::<ParameterizedJsonSchema>() {
|
||||
(parameterized_json_schema.add_and_get_ref)(&mut generator, schema_params, cx);
|
||||
}
|
||||
|
||||
const ZED_SETTINGS: &str = "ZedSettings";
|
||||
let RootSchema {
|
||||
meta_schema,
|
||||
schema: zed_settings_schema,
|
||||
mut definitions,
|
||||
} = combined_schema;
|
||||
definitions.insert(ZED_SETTINGS.to_string(), zed_settings_schema.into());
|
||||
let zed_settings_ref = Schema::new_ref(format!("#/definitions/{ZED_SETTINGS}"));
|
||||
let old_zed_settings_definition = generator
|
||||
.definitions_mut()
|
||||
.insert(ZED_SETTINGS.to_string(), combined_schema);
|
||||
assert_eq!(old_zed_settings_definition, None);
|
||||
let zed_settings_ref = schemars::Schema::new_ref(format!("#/definitions/{ZED_SETTINGS}"));
|
||||
|
||||
let mut root_schema = if let Some(meta_schema) = generator.settings().meta_schema.as_ref() {
|
||||
json_schema!({ "$schema": meta_schema.to_string() })
|
||||
} else {
|
||||
json_schema!({})
|
||||
};
|
||||
|
||||
// settings file contents matches ZedSettings + overrides for each release stage
|
||||
let mut root_schema = json!({
|
||||
"allOf": [
|
||||
root_schema.insert(
|
||||
"allOf".to_string(),
|
||||
json!([
|
||||
zed_settings_ref,
|
||||
{
|
||||
"properties": {
|
||||
|
@ -988,17 +1018,14 @@ impl SettingsStore {
|
|||
"preview": zed_settings_ref,
|
||||
}
|
||||
}
|
||||
],
|
||||
"definitions": definitions,
|
||||
});
|
||||
]),
|
||||
);
|
||||
root_schema.insert(
|
||||
"definitions".to_string(),
|
||||
generator.take_definitions(true).into(),
|
||||
);
|
||||
|
||||
if let Some(meta_schema) = meta_schema {
|
||||
if let Some(root_schema_object) = root_schema.as_object_mut() {
|
||||
root_schema_object.insert("$schema".to_string(), meta_schema.into());
|
||||
}
|
||||
}
|
||||
|
||||
root_schema
|
||||
root_schema.to_value()
|
||||
}
|
||||
|
||||
fn recompute_values(
|
||||
|
@ -1311,13 +1338,8 @@ impl<T: Settings> AnySettingValue for SettingValue<T> {
|
|||
}
|
||||
}
|
||||
|
||||
fn json_schema(
|
||||
&self,
|
||||
generator: &mut SchemaGenerator,
|
||||
params: &SettingsJsonSchemaParams,
|
||||
cx: &App,
|
||||
) -> RootSchema {
|
||||
T::json_schema(generator, params, cx)
|
||||
fn json_schema(&self, generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
T::FileContent::json_schema(generator)
|
||||
}
|
||||
|
||||
fn edits_for_update(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue