uv/crates/uv-configuration/src/config_settings.rs
Charlie Marsh 1aa8ff8268
Merge user and workspace settings (#3462)
## Summary

This PR follows Cargo's strategy for merging configuration, albeit in a
more limited way (we don't support as many configuration locations).
Specifically, we merge the user configuration with the workspace
configuration if both are present. The workspace configuration has
priority, such that we take values from the workspace configuration and
ignore those in the user configuration if both are specified for a given
setting -- with the exception of arrays and maps, which are
concatenated.

For now, if a user provides a configuration file with `--config-file`,
we _don't_ merge in the user settings.

See:
https://doc.rust-lang.org/cargo/reference/config.html#hierarchical-structure.

Closes #3420.
2024-05-08 14:49:43 -04:00

271 lines
9 KiB
Rust

use std::{
collections::{btree_map::Entry, BTreeMap},
str::FromStr,
};
#[derive(Debug, Clone)]
pub struct ConfigSettingEntry {
/// The key of the setting. For example, given `key=value`, this would be `key`.
key: String,
/// The value of the setting. For example, given `key=value`, this would be `value`.
value: String,
}
impl FromStr for ConfigSettingEntry {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let Some((key, value)) = s.split_once('=') else {
return Err(anyhow::anyhow!(
"Invalid config setting: {s} (expected `KEY=VALUE`)"
));
};
Ok(Self {
key: key.trim().to_string(),
value: value.trim().to_string(),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
enum ConfigSettingValue {
/// The value consists of a single string.
String(String),
/// The value consists of a list of strings.
List(Vec<String>),
}
impl serde::Serialize for ConfigSettingValue {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
ConfigSettingValue::String(value) => serializer.serialize_str(value),
ConfigSettingValue::List(values) => serializer.collect_seq(values.iter()),
}
}
}
impl<'de> serde::Deserialize<'de> for ConfigSettingValue {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = ConfigSettingValue;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a string or list of strings")
}
fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
Ok(ConfigSettingValue::String(value.to_string()))
}
fn visit_seq<A: serde::de::SeqAccess<'de>>(
self,
mut seq: A,
) -> Result<Self::Value, A::Error> {
let mut values = Vec::new();
while let Some(value) = seq.next_element()? {
values.push(value);
}
Ok(ConfigSettingValue::List(values))
}
}
deserializer.deserialize_any(Visitor)
}
}
/// Settings to pass to a PEP 517 build backend, structured as a map from (string) key to string or
/// list of strings.
///
/// See: <https://peps.python.org/pep-0517/#config-settings>
#[derive(Debug, Default, Clone)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ConfigSettings(BTreeMap<String, ConfigSettingValue>);
impl FromIterator<ConfigSettingEntry> for ConfigSettings {
fn from_iter<T: IntoIterator<Item = ConfigSettingEntry>>(iter: T) -> Self {
let mut config = BTreeMap::default();
for entry in iter {
match config.entry(entry.key) {
Entry::Vacant(vacant) => {
vacant.insert(ConfigSettingValue::String(entry.value));
}
Entry::Occupied(mut occupied) => match occupied.get_mut() {
ConfigSettingValue::String(existing) => {
let existing = existing.clone();
occupied.insert(ConfigSettingValue::List(vec![existing, entry.value]));
}
ConfigSettingValue::List(existing) => {
existing.push(entry.value);
}
},
}
}
Self(config)
}
}
impl ConfigSettings {
/// Convert the settings to a string that can be passed directly to a PEP 517 build backend.
pub fn escape_for_python(&self) -> String {
serde_json::to_string(self).expect("Failed to serialize config settings")
}
/// Merge two sets of config settings, with the values in `self` taking precedence.
#[must_use]
pub fn merge(self, other: ConfigSettings) -> ConfigSettings {
let mut config = self.0;
for (key, value) in other.0 {
match config.entry(key) {
Entry::Vacant(vacant) => {
vacant.insert(value);
}
Entry::Occupied(mut occupied) => match occupied.get_mut() {
ConfigSettingValue::String(existing) => {
let existing = existing.clone();
match value {
ConfigSettingValue::String(value) => {
occupied.insert(ConfigSettingValue::List(vec![existing, value]));
}
ConfigSettingValue::List(mut values) => {
values.insert(0, existing);
occupied.insert(ConfigSettingValue::List(values));
}
}
}
ConfigSettingValue::List(existing) => match value {
ConfigSettingValue::String(value) => {
existing.push(value);
}
ConfigSettingValue::List(values) => {
existing.extend(values);
}
},
},
}
}
Self(config)
}
}
impl serde::Serialize for ConfigSettings {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeMap;
let mut map = serializer.serialize_map(Some(self.0.len()))?;
for (key, value) in &self.0 {
map.serialize_entry(key, value)?;
}
map.end()
}
}
impl<'de> serde::Deserialize<'de> for ConfigSettings {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = ConfigSettings;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a map from string to string or list of strings")
}
fn visit_map<A: serde::de::MapAccess<'de>>(
self,
mut map: A,
) -> Result<Self::Value, A::Error> {
let mut config = BTreeMap::default();
while let Some((key, value)) = map.next_entry()? {
config.insert(key, value);
}
Ok(ConfigSettings(config))
}
}
deserializer.deserialize_map(Visitor)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn collect_config_settings() {
let settings: ConfigSettings = vec![
ConfigSettingEntry {
key: "key".to_string(),
value: "value".to_string(),
},
ConfigSettingEntry {
key: "key".to_string(),
value: "value2".to_string(),
},
ConfigSettingEntry {
key: "list".to_string(),
value: "value3".to_string(),
},
ConfigSettingEntry {
key: "list".to_string(),
value: "value4".to_string(),
},
]
.into_iter()
.collect();
assert_eq!(
settings.0.get("key"),
Some(&ConfigSettingValue::List(vec![
"value".to_string(),
"value2".to_string()
]))
);
assert_eq!(
settings.0.get("list"),
Some(&ConfigSettingValue::List(vec![
"value3".to_string(),
"value4".to_string()
]))
);
}
#[test]
fn escape_for_python() {
let mut settings = ConfigSettings::default();
settings.0.insert(
"key".to_string(),
ConfigSettingValue::String("value".to_string()),
);
settings.0.insert(
"list".to_string(),
ConfigSettingValue::List(vec!["value1".to_string(), "value2".to_string()]),
);
assert_eq!(
settings.escape_for_python(),
r#"{"key":"value","list":["value1","value2"]}"#
);
let mut settings = ConfigSettings::default();
settings.0.insert(
"key".to_string(),
ConfigSettingValue::String("Hello, \"world!\"".to_string()),
);
settings.0.insert(
"list".to_string(),
ConfigSettingValue::List(vec!["'value1'".to_string()]),
);
assert_eq!(
settings.escape_for_python(),
r#"{"key":"Hello, \"world!\"","list":["'value1'"]}"#
);
let mut settings = ConfigSettings::default();
settings.0.insert(
"key".to_string(),
ConfigSettingValue::String("val\\1 {}value".to_string()),
);
assert_eq!(settings.escape_for_python(), r#"{"key":"val\\1 {}value"}"#);
}
}