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 is_empty(&self) -> bool {
141 self.0.is_empty()
142 }
143
144 pub fn escape_for_python(&self) -> String {
146 serde_json::to_string(self).expect("Failed to serialize config settings")
147 }
148
149 #[must_use]
151 pub fn merge(self, other: Self) -> Self {
152 let mut config = self.0;
153 for (key, value) in other.0 {
154 match config.entry(key) {
155 Entry::Vacant(vacant) => {
156 vacant.insert(value);
157 }
158 Entry::Occupied(mut occupied) => match occupied.get_mut() {
159 ConfigSettingValue::String(existing) => {
160 let existing = existing.clone();
161 match value {
162 ConfigSettingValue::String(value) => {
163 occupied.insert(ConfigSettingValue::List(vec![existing, value]));
164 }
165 ConfigSettingValue::List(mut values) => {
166 values.insert(0, existing);
167 occupied.insert(ConfigSettingValue::List(values));
168 }
169 }
170 }
171 ConfigSettingValue::List(existing) => match value {
172 ConfigSettingValue::String(value) => {
173 existing.push(value);
174 }
175 ConfigSettingValue::List(values) => {
176 existing.extend(values);
177 }
178 },
179 },
180 }
181 }
182 Self(config)
183 }
184}
185
186impl uv_cache_key::CacheKey for ConfigSettings {
187 fn cache_key(&self, state: &mut CacheKeyHasher) {
188 for (key, value) in &self.0 {
189 key.cache_key(state);
190 match value {
191 ConfigSettingValue::String(value) => value.cache_key(state),
192 ConfigSettingValue::List(values) => values.cache_key(state),
193 }
194 }
195 }
196}
197
198impl serde::Serialize for ConfigSettings {
199 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
200 use serde::ser::SerializeMap;
201
202 let mut map = serializer.serialize_map(Some(self.0.len()))?;
203 for (key, value) in &self.0 {
204 map.serialize_entry(key, value)?;
205 }
206 map.end()
207 }
208}
209
210impl<'de> serde::Deserialize<'de> for ConfigSettings {
211 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
212 struct Visitor;
213
214 impl<'de> serde::de::Visitor<'de> for Visitor {
215 type Value = ConfigSettings;
216
217 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
218 formatter.write_str("a map from string to string or list of strings")
219 }
220
221 fn visit_map<A: serde::de::MapAccess<'de>>(
222 self,
223 mut map: A,
224 ) -> Result<Self::Value, A::Error> {
225 let mut config = BTreeMap::default();
226 while let Some((key, value)) = map.next_entry()? {
227 config.insert(key, value);
228 }
229 Ok(ConfigSettings(config))
230 }
231 }
232
233 deserializer.deserialize_map(Visitor)
234 }
235}
236
237#[derive(Debug, Default, Clone, PartialEq, Eq)]
239#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
240pub struct PackageConfigSettings(BTreeMap<PackageName, ConfigSettings>);
241
242impl FromIterator<ConfigSettingPackageEntry> for PackageConfigSettings {
243 fn from_iter<T: IntoIterator<Item = ConfigSettingPackageEntry>>(iter: T) -> Self {
244 let mut package_configs: BTreeMap<PackageName, Vec<ConfigSettingEntry>> = BTreeMap::new();
245
246 for entry in iter {
247 package_configs
248 .entry(entry.package)
249 .or_default()
250 .push(entry.setting);
251 }
252
253 let configs = package_configs
254 .into_iter()
255 .map(|(package, entries)| (package, entries.into_iter().collect()))
256 .collect();
257
258 Self(configs)
259 }
260}
261
262impl PackageConfigSettings {
263 pub fn get(&self, package: &PackageName) -> Option<&ConfigSettings> {
265 self.0.get(package)
266 }
267
268 #[must_use]
270 pub fn merge(mut self, other: Self) -> Self {
271 for (package, settings) in other.0 {
272 match self.0.entry(package) {
273 Entry::Vacant(vacant) => {
274 vacant.insert(settings);
275 }
276 Entry::Occupied(mut occupied) => {
277 let merged = occupied.get().clone().merge(settings);
278 occupied.insert(merged);
279 }
280 }
281 }
282 self
283 }
284}
285
286impl uv_cache_key::CacheKey for PackageConfigSettings {
287 fn cache_key(&self, state: &mut CacheKeyHasher) {
288 for (package, settings) in &self.0 {
289 package.to_string().cache_key(state);
290 settings.cache_key(state);
291 }
292 }
293}
294
295impl serde::Serialize for PackageConfigSettings {
296 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
297 use serde::ser::SerializeMap;
298
299 let mut map = serializer.serialize_map(Some(self.0.len()))?;
300 for (key, value) in &self.0 {
301 map.serialize_entry(&key.to_string(), value)?;
302 }
303 map.end()
304 }
305}
306
307impl<'de> serde::Deserialize<'de> for PackageConfigSettings {
308 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
309 struct Visitor;
310
311 impl<'de> serde::de::Visitor<'de> for Visitor {
312 type Value = PackageConfigSettings;
313
314 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
315 formatter.write_str("a map from package name to config settings")
316 }
317
318 fn visit_map<A: serde::de::MapAccess<'de>>(
319 self,
320 mut map: A,
321 ) -> Result<Self::Value, A::Error> {
322 let mut config = BTreeMap::default();
323 while let Some((key, value)) = map.next_entry::<String, ConfigSettings>()? {
324 let package = PackageName::from_str(&key).map_err(|e| {
325 serde::de::Error::custom(format!("Invalid package name: {e}"))
326 })?;
327 config.insert(package, value);
328 }
329 Ok(PackageConfigSettings(config))
330 }
331 }
332
333 deserializer.deserialize_map(Visitor)
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340
341 #[test]
342 fn collect_config_settings() {
343 let settings: ConfigSettings = vec![
344 ConfigSettingEntry {
345 key: "key".to_string(),
346 value: "value".to_string(),
347 },
348 ConfigSettingEntry {
349 key: "key".to_string(),
350 value: "value2".to_string(),
351 },
352 ConfigSettingEntry {
353 key: "list".to_string(),
354 value: "value3".to_string(),
355 },
356 ConfigSettingEntry {
357 key: "list".to_string(),
358 value: "value4".to_string(),
359 },
360 ]
361 .into_iter()
362 .collect();
363 assert_eq!(
364 settings.0.get("key"),
365 Some(&ConfigSettingValue::List(vec![
366 "value".to_string(),
367 "value2".to_string()
368 ]))
369 );
370 assert_eq!(
371 settings.0.get("list"),
372 Some(&ConfigSettingValue::List(vec![
373 "value3".to_string(),
374 "value4".to_string()
375 ]))
376 );
377 }
378
379 #[test]
380 fn escape_for_python() {
381 let mut settings = ConfigSettings::default();
382 settings.0.insert(
383 "key".to_string(),
384 ConfigSettingValue::String("value".to_string()),
385 );
386 settings.0.insert(
387 "list".to_string(),
388 ConfigSettingValue::List(vec!["value1".to_string(), "value2".to_string()]),
389 );
390 assert_eq!(
391 settings.escape_for_python(),
392 r#"{"key":"value","list":["value1","value2"]}"#
393 );
394
395 let mut settings = ConfigSettings::default();
396 settings.0.insert(
397 "key".to_string(),
398 ConfigSettingValue::String("Hello, \"world!\"".to_string()),
399 );
400 settings.0.insert(
401 "list".to_string(),
402 ConfigSettingValue::List(vec!["'value1'".to_string()]),
403 );
404 assert_eq!(
405 settings.escape_for_python(),
406 r#"{"key":"Hello, \"world!\"","list":["'value1'"]}"#
407 );
408
409 let mut settings = ConfigSettings::default();
410 settings.0.insert(
411 "key".to_string(),
412 ConfigSettingValue::String("val\\1 {}value".to_string()),
413 );
414 assert_eq!(settings.escape_for_python(), r#"{"key":"val\\1 {}value"}"#);
415 }
416
417 #[test]
418 fn parse_config_setting_package_entry() {
419 let entry = ConfigSettingPackageEntry::from_str("numpy:editable_mode=compat").unwrap();
421 assert_eq!(entry.package.as_ref(), "numpy");
422 assert_eq!(entry.setting.key, "editable_mode");
423 assert_eq!(entry.setting.value, "compat");
424
425 let entry = ConfigSettingPackageEntry::from_str("my-package:some_key=value").unwrap();
427 assert_eq!(entry.package.as_ref(), "my-package");
428 assert_eq!(entry.setting.key, "some_key");
429 assert_eq!(entry.setting.value, "value");
430
431 let entry = ConfigSettingPackageEntry::from_str(" numpy : key = value ").unwrap();
433 assert_eq!(entry.package.as_ref(), "numpy");
434 assert_eq!(entry.setting.key, "key");
435 assert_eq!(entry.setting.value, "value");
436 }
437
438 #[test]
439 fn collect_config_settings_package() {
440 let settings: PackageConfigSettings = vec![
441 ConfigSettingPackageEntry::from_str("numpy:editable_mode=compat").unwrap(),
442 ConfigSettingPackageEntry::from_str("numpy:another_key=value").unwrap(),
443 ConfigSettingPackageEntry::from_str("scipy:build_option=fast").unwrap(),
444 ]
445 .into_iter()
446 .collect();
447
448 let numpy_settings = settings
449 .get(&PackageName::from_str("numpy").unwrap())
450 .unwrap();
451 assert_eq!(
452 numpy_settings.0.get("editable_mode"),
453 Some(&ConfigSettingValue::String("compat".to_string()))
454 );
455 assert_eq!(
456 numpy_settings.0.get("another_key"),
457 Some(&ConfigSettingValue::String("value".to_string()))
458 );
459
460 let scipy_settings = settings
461 .get(&PackageName::from_str("scipy").unwrap())
462 .unwrap();
463 assert_eq!(
464 scipy_settings.0.get("build_option"),
465 Some(&ConfigSettingValue::String("fast".to_string()))
466 );
467 }
468}