1use std::{
2 collections::{BTreeMap, btree_map::Entry},
3 str::FromStr,
4};
5use uv_cache_key::CacheKeyHasher;
6use uv_normalize::PackageName;
7
8#[derive(Debug, Clone)]
9pub struct ConfigSettingEntry {
10 key: String,
12 value: String,
14}
15
16impl FromStr for ConfigSettingEntry {
17 type Err = String;
18
19 fn from_str(s: &str) -> Result<Self, Self::Err> {
20 let Some((key, value)) = s.split_once('=') else {
21 return Err(format!(
22 "Invalid config setting: {s} (expected `KEY=VALUE`)"
23 ));
24 };
25 Ok(Self {
26 key: key.trim().to_string(),
27 value: value.trim().to_string(),
28 })
29 }
30}
31
32#[derive(Debug, Clone)]
33pub struct ConfigSettingPackageEntry {
34 package: PackageName,
36 setting: ConfigSettingEntry,
38}
39
40impl FromStr for ConfigSettingPackageEntry {
41 type Err = String;
42
43 fn from_str(s: &str) -> Result<Self, Self::Err> {
44 let Some((package_str, config_str)) = s.split_once(':') else {
45 return Err(format!(
46 "Invalid config setting: {s} (expected `PACKAGE:KEY=VALUE`)"
47 ));
48 };
49
50 let package = PackageName::from_str(package_str.trim())
51 .map_err(|e| format!("Invalid package name: {e}"))?;
52 let setting = ConfigSettingEntry::from_str(config_str)?;
53
54 Ok(Self { package, setting })
55 }
56}
57
58#[derive(Debug, Clone, Hash, PartialEq, Eq)]
59#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema), schemars(untagged))]
60enum ConfigSettingValue {
61 String(String),
63 List(Vec<String>),
65}
66
67impl serde::Serialize for ConfigSettingValue {
68 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
69 match self {
70 Self::String(value) => serializer.serialize_str(value),
71 Self::List(values) => serializer.collect_seq(values.iter()),
72 }
73 }
74}
75
76impl<'de> serde::Deserialize<'de> for ConfigSettingValue {
77 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
78 struct Visitor;
79
80 impl<'de> serde::de::Visitor<'de> for Visitor {
81 type Value = ConfigSettingValue;
82
83 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
84 formatter.write_str("a string or list of strings")
85 }
86
87 fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
88 Ok(ConfigSettingValue::String(value.to_string()))
89 }
90
91 fn visit_seq<A: serde::de::SeqAccess<'de>>(
92 self,
93 mut seq: A,
94 ) -> Result<Self::Value, A::Error> {
95 let mut values = Vec::new();
96 while let Some(value) = seq.next_element()? {
97 values.push(value);
98 }
99 Ok(ConfigSettingValue::List(values))
100 }
101 }
102
103 deserializer.deserialize_any(Visitor)
104 }
105}
106
107#[derive(Debug, Default, Hash, Clone, PartialEq, Eq)]
112#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
113pub struct ConfigSettings(BTreeMap<String, ConfigSettingValue>);
114
115impl FromIterator<ConfigSettingEntry> for ConfigSettings {
116 fn from_iter<T: IntoIterator<Item = ConfigSettingEntry>>(iter: T) -> Self {
117 let mut config = BTreeMap::default();
118 for entry in iter {
119 match config.entry(entry.key) {
120 Entry::Vacant(vacant) => {
121 vacant.insert(ConfigSettingValue::String(entry.value));
122 }
123 Entry::Occupied(mut occupied) => match occupied.get_mut() {
124 ConfigSettingValue::String(existing) => {
125 let existing = existing.clone();
126 occupied.insert(ConfigSettingValue::List(vec![existing, entry.value]));
127 }
128 ConfigSettingValue::List(existing) => {
129 existing.push(entry.value);
130 }
131 },
132 }
133 }
134 Self(config)
135 }
136}
137
138impl ConfigSettings {
139 pub fn len(&self) -> usize {
141 self.0.len()
142 }
143
144 pub fn is_empty(&self) -> bool {
146 self.0.is_empty()
147 }
148
149 pub fn escape_for_python(&self) -> String {
151 serde_json::to_string(self).expect("Failed to serialize config settings")
152 }
153
154 #[must_use]
156 pub fn merge(self, other: Self) -> Self {
157 let mut config = self.0;
158 for (key, value) in other.0 {
159 match config.entry(key) {
160 Entry::Vacant(vacant) => {
161 vacant.insert(value);
162 }
163 Entry::Occupied(mut occupied) => match occupied.get_mut() {
164 ConfigSettingValue::String(existing) => {
165 let existing = existing.clone();
166 match value {
167 ConfigSettingValue::String(value) => {
168 occupied.insert(ConfigSettingValue::List(vec![existing, value]));
169 }
170 ConfigSettingValue::List(mut values) => {
171 values.insert(0, existing);
172 occupied.insert(ConfigSettingValue::List(values));
173 }
174 }
175 }
176 ConfigSettingValue::List(existing) => match value {
177 ConfigSettingValue::String(value) => {
178 existing.push(value);
179 }
180 ConfigSettingValue::List(values) => {
181 existing.extend(values);
182 }
183 },
184 },
185 }
186 }
187 Self(config)
188 }
189}
190
191impl uv_cache_key::CacheKey for ConfigSettings {
192 fn cache_key(&self, state: &mut CacheKeyHasher) {
193 for (key, value) in &self.0 {
194 key.cache_key(state);
195 match value {
196 ConfigSettingValue::String(value) => value.cache_key(state),
197 ConfigSettingValue::List(values) => values.cache_key(state),
198 }
199 }
200 }
201}
202
203impl serde::Serialize for ConfigSettings {
204 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
205 use serde::ser::SerializeMap;
206
207 let mut map = serializer.serialize_map(Some(self.0.len()))?;
208 for (key, value) in &self.0 {
209 map.serialize_entry(key, value)?;
210 }
211 map.end()
212 }
213}
214
215impl<'de> serde::Deserialize<'de> for ConfigSettings {
216 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
217 struct Visitor;
218
219 impl<'de> serde::de::Visitor<'de> for Visitor {
220 type Value = ConfigSettings;
221
222 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
223 formatter.write_str("a map from string to string or list of strings")
224 }
225
226 fn visit_map<A: serde::de::MapAccess<'de>>(
227 self,
228 mut map: A,
229 ) -> Result<Self::Value, A::Error> {
230 let mut config = BTreeMap::default();
231 while let Some((key, value)) = map.next_entry()? {
232 config.insert(key, value);
233 }
234 Ok(ConfigSettings(config))
235 }
236 }
237
238 deserializer.deserialize_map(Visitor)
239 }
240}
241
242#[derive(Debug, Default, Clone, PartialEq, Eq)]
244#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
245pub struct PackageConfigSettings(BTreeMap<PackageName, ConfigSettings>);
246
247impl FromIterator<ConfigSettingPackageEntry> for PackageConfigSettings {
248 fn from_iter<T: IntoIterator<Item = ConfigSettingPackageEntry>>(iter: T) -> Self {
249 let mut package_configs: BTreeMap<PackageName, Vec<ConfigSettingEntry>> = BTreeMap::new();
250
251 for entry in iter {
252 package_configs
253 .entry(entry.package)
254 .or_default()
255 .push(entry.setting);
256 }
257
258 let configs = package_configs
259 .into_iter()
260 .map(|(package, entries)| (package, entries.into_iter().collect()))
261 .collect();
262
263 Self(configs)
264 }
265}
266
267impl PackageConfigSettings {
268 pub fn get(&self, package: &PackageName) -> Option<&ConfigSettings> {
270 self.0.get(package)
271 }
272
273 pub fn is_empty(&self) -> bool {
275 self.0.is_empty()
276 }
277
278 #[must_use]
280 pub fn merge(mut self, other: Self) -> Self {
281 for (package, settings) in other.0 {
282 match self.0.entry(package) {
283 Entry::Vacant(vacant) => {
284 vacant.insert(settings);
285 }
286 Entry::Occupied(mut occupied) => {
287 let merged = occupied.get().clone().merge(settings);
288 occupied.insert(merged);
289 }
290 }
291 }
292 self
293 }
294}
295
296impl uv_cache_key::CacheKey for PackageConfigSettings {
297 fn cache_key(&self, state: &mut CacheKeyHasher) {
298 for (package, settings) in &self.0 {
299 package.to_string().cache_key(state);
300 settings.cache_key(state);
301 }
302 }
303}
304
305impl serde::Serialize for PackageConfigSettings {
306 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
307 use serde::ser::SerializeMap;
308
309 let mut map = serializer.serialize_map(Some(self.0.len()))?;
310 for (key, value) in &self.0 {
311 map.serialize_entry(&key.to_string(), value)?;
312 }
313 map.end()
314 }
315}
316
317impl<'de> serde::Deserialize<'de> for PackageConfigSettings {
318 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
319 struct Visitor;
320
321 impl<'de> serde::de::Visitor<'de> for Visitor {
322 type Value = PackageConfigSettings;
323
324 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
325 formatter.write_str("a map from package name to config settings")
326 }
327
328 fn visit_map<A: serde::de::MapAccess<'de>>(
329 self,
330 mut map: A,
331 ) -> Result<Self::Value, A::Error> {
332 let mut config = BTreeMap::default();
333 while let Some((key, value)) = map.next_entry::<String, ConfigSettings>()? {
334 let package = PackageName::from_str(&key).map_err(|e| {
335 serde::de::Error::custom(format!("Invalid package name: {e}"))
336 })?;
337 config.insert(package, value);
338 }
339 Ok(PackageConfigSettings(config))
340 }
341 }
342
343 deserializer.deserialize_map(Visitor)
344 }
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350
351 #[test]
352 fn collect_config_settings() {
353 let settings: ConfigSettings = vec![
354 ConfigSettingEntry {
355 key: "key".to_string(),
356 value: "value".to_string(),
357 },
358 ConfigSettingEntry {
359 key: "key".to_string(),
360 value: "value2".to_string(),
361 },
362 ConfigSettingEntry {
363 key: "list".to_string(),
364 value: "value3".to_string(),
365 },
366 ConfigSettingEntry {
367 key: "list".to_string(),
368 value: "value4".to_string(),
369 },
370 ]
371 .into_iter()
372 .collect();
373 assert_eq!(
374 settings.0.get("key"),
375 Some(&ConfigSettingValue::List(vec![
376 "value".to_string(),
377 "value2".to_string()
378 ]))
379 );
380 assert_eq!(
381 settings.0.get("list"),
382 Some(&ConfigSettingValue::List(vec![
383 "value3".to_string(),
384 "value4".to_string()
385 ]))
386 );
387 }
388
389 #[test]
390 fn escape_for_python() {
391 let mut settings = ConfigSettings::default();
392 settings.0.insert(
393 "key".to_string(),
394 ConfigSettingValue::String("value".to_string()),
395 );
396 settings.0.insert(
397 "list".to_string(),
398 ConfigSettingValue::List(vec!["value1".to_string(), "value2".to_string()]),
399 );
400 assert_eq!(
401 settings.escape_for_python(),
402 r#"{"key":"value","list":["value1","value2"]}"#
403 );
404
405 let mut settings = ConfigSettings::default();
406 settings.0.insert(
407 "key".to_string(),
408 ConfigSettingValue::String("Hello, \"world!\"".to_string()),
409 );
410 settings.0.insert(
411 "list".to_string(),
412 ConfigSettingValue::List(vec!["'value1'".to_string()]),
413 );
414 assert_eq!(
415 settings.escape_for_python(),
416 r#"{"key":"Hello, \"world!\"","list":["'value1'"]}"#
417 );
418
419 let mut settings = ConfigSettings::default();
420 settings.0.insert(
421 "key".to_string(),
422 ConfigSettingValue::String("val\\1 {}value".to_string()),
423 );
424 assert_eq!(settings.escape_for_python(), r#"{"key":"val\\1 {}value"}"#);
425 }
426
427 #[test]
428 fn parse_config_setting_package_entry() {
429 let entry = ConfigSettingPackageEntry::from_str("numpy:editable_mode=compat").unwrap();
431 assert_eq!(entry.package.as_ref(), "numpy");
432 assert_eq!(entry.setting.key, "editable_mode");
433 assert_eq!(entry.setting.value, "compat");
434
435 let entry = ConfigSettingPackageEntry::from_str("my-package:some_key=value").unwrap();
437 assert_eq!(entry.package.as_ref(), "my-package");
438 assert_eq!(entry.setting.key, "some_key");
439 assert_eq!(entry.setting.value, "value");
440
441 let entry = ConfigSettingPackageEntry::from_str(" numpy : key = value ").unwrap();
443 assert_eq!(entry.package.as_ref(), "numpy");
444 assert_eq!(entry.setting.key, "key");
445 assert_eq!(entry.setting.value, "value");
446 }
447
448 #[test]
449 fn collect_config_settings_package() {
450 let settings: PackageConfigSettings = vec![
451 ConfigSettingPackageEntry::from_str("numpy:editable_mode=compat").unwrap(),
452 ConfigSettingPackageEntry::from_str("numpy:another_key=value").unwrap(),
453 ConfigSettingPackageEntry::from_str("scipy:build_option=fast").unwrap(),
454 ]
455 .into_iter()
456 .collect();
457
458 let numpy_settings = settings
459 .get(&PackageName::from_str("numpy").unwrap())
460 .unwrap();
461 assert_eq!(
462 numpy_settings.0.get("editable_mode"),
463 Some(&ConfigSettingValue::String("compat".to_string()))
464 );
465 assert_eq!(
466 numpy_settings.0.get("another_key"),
467 Some(&ConfigSettingValue::String("value".to_string()))
468 );
469
470 let scipy_settings = settings
471 .get(&PackageName::from_str("scipy").unwrap())
472 .unwrap();
473 assert_eq!(
474 scipy_settings.0.get("build_option"),
475 Some(&ConfigSettingValue::String("fast".to_string()))
476 );
477 }
478}