use anyhow::{bail, Result};
use serde_json::{json, Value};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum Features {
#[default]
Default,
All,
Named(Vec<String>),
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Settings {
features: Features,
no_default_features: bool,
overrides: Vec<(String, Value)>,
}
impl Settings {
pub fn enable_all_features(&mut self) -> Result<()> {
if self.features != Features::Default {
bail!("--all-features cannot be combined with --features");
}
self.features = Features::All;
Ok(())
}
pub fn enable_features(&mut self, list: &str) -> Result<()> {
let named: Vec<String> = list
.split([',', ' ', '\t'])
.filter(|feature| !feature.is_empty())
.map(str::to_string)
.collect();
if named.is_empty() {
bail!("--features needs at least one feature name");
}
match &mut self.features {
Features::All => bail!("--features cannot be combined with --all-features"),
Features::Named(features) => features.extend(named),
features @ Features::Default => *features = Features::Named(named),
}
Ok(())
}
pub fn disable_default_features(&mut self) -> Result<()> {
if self.features == Features::All {
bail!("--no-default-features cannot be combined with --all-features");
}
self.no_default_features = true;
Ok(())
}
pub fn set(&mut self, assignment: &str) -> Result<()> {
let Some((key, value)) = assignment.split_once('=') else {
bail!("--config needs a KEY=VALUE, such as check.command=clippy");
};
if key.is_empty() || key.split('.').any(str::is_empty) {
bail!("'{key}' is not a setting name");
}
let value = serde_json::from_str(value).unwrap_or_else(|_| json!(value));
self.overrides.push((key.to_string(), value));
Ok(())
}
pub fn to_json(&self) -> Value {
let mut settings = json!({
"cargo": {
"buildScripts": {
"enable": true
}
},
"checkOnSave": true,
"diagnostics": {
"enable": true,
"experimental": {
"enable": true
}
},
"procMacro": {
"enable": true
}
});
match &self.features {
Features::Default => {}
Features::All => settings["cargo"]["features"] = json!("all"),
Features::Named(features) => settings["cargo"]["features"] = json!(features),
}
if self.no_default_features {
settings["cargo"]["noDefaultFeatures"] = json!(true);
}
for (key, value) in &self.overrides {
set_at(&mut settings, key, value.clone());
}
settings
}
}
fn set_at(settings: &mut Value, key: &str, value: Value) {
let mut at = settings;
let mut names = key.split('.').peekable();
while let Some(name) = names.next() {
if names.peek().is_none() {
at[name] = value;
return;
}
if !at[name].is_object() {
at[name] = json!({});
}
at = &mut at[name];
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_defaults_leave_features_to_the_manifest() {
let settings = Settings::default().to_json();
assert_eq!(settings["cargo"]["features"], Value::Null);
assert_eq!(settings["cargo"]["noDefaultFeatures"], Value::Null);
}
#[test]
fn all_features_is_the_word_rust_analyzer_knows() {
let mut settings = Settings::default();
settings.enable_all_features().unwrap();
assert_eq!(settings.to_json()["cargo"]["features"], json!("all"));
}
#[test]
fn features_are_named_one_by_one() {
let mut settings = Settings::default();
settings.enable_features("serde,tokio").unwrap();
settings.enable_features("other/thing extra").unwrap();
assert_eq!(
settings.to_json()["cargo"]["features"],
json!(["serde", "tokio", "other/thing", "extra"])
);
}
#[test]
fn features_that_name_nothing_are_refused() {
assert!(Settings::default().enable_features("").is_err());
assert!(Settings::default().enable_features(" , ").is_err());
}
#[test]
fn all_features_cannot_be_narrowed() {
let mut settings = Settings::default();
settings.enable_all_features().unwrap();
assert!(settings.enable_features("serde").is_err());
assert!(settings.disable_default_features().is_err());
let mut settings = Settings::default();
settings.enable_features("serde").unwrap();
assert!(settings.enable_all_features().is_err());
}
#[test]
fn default_features_can_be_left_out() {
let mut settings = Settings::default();
settings.disable_default_features().unwrap();
settings.enable_features("serde").unwrap();
let settings = settings.to_json();
assert_eq!(settings["cargo"]["noDefaultFeatures"], json!(true));
assert_eq!(settings["cargo"]["features"], json!(["serde"]));
}
#[test]
fn any_setting_can_be_named_outright() {
let mut settings = Settings::default();
settings.set("check.command=clippy").unwrap();
settings
.set("cargo.target=x86_64-unknown-linux-gnu")
.unwrap();
settings.set("check.extraArgs=[\"--tests\"]").unwrap();
settings.set("procMacro.enable=false").unwrap();
let settings = settings.to_json();
assert_eq!(settings["check"]["command"], json!("clippy"));
assert_eq!(
settings["cargo"]["target"],
json!("x86_64-unknown-linux-gnu")
);
assert_eq!(settings["check"]["extraArgs"], json!(["--tests"]));
assert_eq!(settings["procMacro"]["enable"], json!(false));
}
#[test]
fn an_override_replaces_what_stands_in_its_way() {
let mut settings = Settings::default();
settings.set("checkOnSave.enable=true").unwrap();
assert_eq!(settings.to_json()["checkOnSave"], json!({ "enable": true }));
}
#[test]
fn settings_that_name_nothing_are_refused() {
assert!(Settings::default().set("check.command").is_err());
assert!(Settings::default().set("=clippy").is_err());
assert!(Settings::default().set("check..command=clippy").is_err());
}
}