use serde::{Deserialize, Deserializer, Serialize};
use serde_json::{Map, Value};
const TOML_DATETIME_KEY: &str = "$__toml_private_datetime";
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum InvalidLspSettings {
#[error("settings must not be empty")]
Empty,
#[error("settings key `{path}` conflicts with another setting")]
Conflict {
path: String,
},
#[error("settings key `{key}` has an empty segment")]
EmptySegment {
key: String,
},
#[error("settings value at `{path}` is a TOML datetime, which is not supported")]
Datetime {
path: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(transparent)]
pub struct LspSettings(Map<String, Value>);
impl LspSettings {
pub fn new(raw: Map<String, Value>) -> Result<Self, InvalidLspSettings> {
if raw.is_empty() {
return Err(InvalidLspSettings::Empty);
}
let mut plain = Map::new();
let mut dotted = Vec::new();
for (key, value) in raw {
reject_datetime(&key, &value)?;
let segments: Vec<String> = key.split('.').map(str::to_owned).collect();
if segments.iter().any(String::is_empty) {
return Err(InvalidLspSettings::EmptySegment { key });
}
if segments.len() > 1 {
dotted.push((segments, value));
} else {
plain.insert(key, value);
}
}
dotted.sort_by_key(|(segments, _)| segments.len());
for (segments, value) in dotted {
insert_at(&mut plain, &segments, value)?;
}
Ok(Self(plain))
}
#[must_use]
pub fn section(&self, section: Option<&str>) -> Value {
let Some(section) = section else {
return self.to_value();
};
let mut segments = section.split('.');
let Some(first) = segments.next() else {
return Value::Null;
};
let mut current = self.0.get(first);
for segment in segments {
current = current.and_then(|value| value.get(segment));
}
current.cloned().unwrap_or(Value::Null)
}
#[must_use]
pub fn to_value(&self) -> Value {
Value::Object(self.0.clone())
}
}
impl<'de> Deserialize<'de> for LspSettings {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let raw = Map::<String, Value>::deserialize(deserializer)?;
Self::new(raw).map_err(serde::de::Error::custom)
}
}
fn insert_at(
root: &mut Map<String, Value>,
segments: &[String],
value: Value,
) -> Result<(), InvalidLspSettings> {
let conflict = || InvalidLspSettings::Conflict {
path: segments.join("."),
};
let Some((last, parents)) = segments.split_last() else {
return Err(conflict());
};
let mut current = root;
for segment in parents {
let entry = current
.entry(segment.clone())
.or_insert_with(|| Value::Object(Map::new()));
current = entry.as_object_mut().ok_or_else(conflict)?;
}
if current.contains_key(last) {
return Err(conflict());
}
current.insert(last.clone(), value);
Ok(())
}
fn reject_datetime(path: &str, value: &Value) -> Result<(), InvalidLspSettings> {
match value {
Value::Object(map) => {
if map.len() == 1 && map.contains_key(TOML_DATETIME_KEY) {
return Err(InvalidLspSettings::Datetime {
path: path.to_owned(),
});
}
map.iter()
.try_for_each(|(key, inner)| reject_datetime(&format!("{path}.{key}"), inner))
}
Value::Array(items) => items
.iter()
.enumerate()
.try_for_each(|(index, inner)| reject_datetime(&format!("{path}[{index}]"), inner)),
_ => Ok(()),
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
fn parse(json: &str) -> Result<LspSettings, serde_json::Error> {
serde_json::from_str(json)
}
#[test]
fn expands_top_level_dotted_keys() {
let settings = parse(r#"{"python.analysis.typeCheckingMode": "strict"}"#).unwrap();
assert_eq!(
settings.to_value(),
json!({"python": {"analysis": {"typeCheckingMode": "strict"}}})
);
}
#[test]
fn merges_sibling_keys_into_existing_object() {
let settings = parse(r#"{"python": {"a": 1}, "python.b": 2}"#).unwrap();
assert_eq!(settings.to_value(), json!({"python": {"a": 1, "b": 2}}));
}
#[test]
fn gopls_flat_keys_round_trip_byte_identical() {
let input = r#"{"gopls":{"ui.semanticTokens":true}}"#;
let settings = parse(input).unwrap();
assert_eq!(serde_json::to_string(&settings).unwrap(), input);
}
#[test]
fn yaml_schemas_url_key_preserved_after_expansion() {
let settings =
parse(r#"{"yaml.schemas": {"https://json.schemastore.org/x.json": "*.yml"}}"#).unwrap();
assert_eq!(
settings.section(Some("yaml.schemas")),
json!({"https://json.schemastore.org/x.json": "*.yml"})
);
assert_eq!(
settings.to_value(),
json!({"yaml": {"schemas": {"https://json.schemastore.org/x.json": "*.yml"}}})
);
}
#[test]
fn rejects_conflict_between_dotted_and_nested() {
let err = parse(r#"{"python.analysis.x": 1, "python": {"analysis": {"x": 2}}}"#)
.unwrap_err()
.to_string();
assert!(err.contains("python.analysis.x"), "{err}");
}
#[test]
fn rejects_non_object_intermediate() {
let raw = json!({"a": 1, "a.b": 2});
let Value::Object(raw) = raw else {
unreachable!()
};
assert_eq!(
LspSettings::new(raw),
Err(InvalidLspSettings::Conflict { path: "a.b".into() })
);
}
#[test]
fn rejects_empty_object() {
let err = parse("{}").unwrap_err().to_string();
assert!(err.contains("must not be empty"), "{err}");
}
#[test]
fn rejects_empty_segment() {
let err = parse(r#"{"a..b": 1}"#).unwrap_err().to_string();
assert!(err.contains("empty segment"), "{err}");
}
#[test]
fn rejects_empty_top_level_key() {
let err = parse(r#"{"": 1}"#).unwrap_err().to_string();
assert!(err.contains("empty segment"), "{err}");
}
#[test]
fn rejects_toml_datetime_anywhere() {
type Wrapper = std::collections::HashMap<String, LspSettings>;
let top = toml::from_str::<Wrapper>("[settings]\nwhen = 1979-05-27T07:32:00Z\n");
assert!(top.unwrap_err().to_string().contains("datetime"));
let nested = toml::from_str::<Wrapper>("[settings.a]\nlist = [1979-05-27]\n");
assert!(nested.unwrap_err().to_string().contains("datetime"));
}
#[test]
fn parses_toml_table_with_quoted_dotted_key() {
#[derive(Deserialize)]
struct Wrapper {
settings: LspSettings,
}
let parsed: Wrapper =
toml::from_str("[settings]\n\"python.analysis.mode\" = \"strict\"\n").unwrap();
assert_eq!(
parsed.settings.section(Some("python.analysis.mode")),
json!("strict")
);
}
#[test]
fn section_walks_dotted_paths() {
let settings = parse(r#"{"a": {"b": {"c": 1}}}"#).unwrap();
assert_eq!(settings.section(Some("a.b")), json!({"c": 1}));
assert_eq!(settings.section(Some("a.x")), Value::Null);
assert_eq!(settings.section(Some("z")), Value::Null);
assert_eq!(settings.section(None), settings.to_value());
}
}