cargo/util/context/
config_value.rs

1//! [`ConfigValue`] represents Cargo configuration values loaded from TOML.
2//!
3//! See [the module-level doc](crate::util::context)
4//! for how configuration is parsed and deserialized.
5
6use std::collections::HashMap;
7use std::collections::hash_map::Entry;
8use std::fmt;
9use std::mem;
10
11use anyhow::Context as _;
12use anyhow::anyhow;
13use anyhow::bail;
14
15use super::ConfigKey;
16use super::Definition;
17use super::key;
18use super::key::KeyOrIdx;
19use crate::CargoResult;
20
21use self::ConfigValue as CV;
22
23/// List of which configuration lists cannot be merged.
24///
25/// Instead of merging,
26/// the higher priority list should replaces the lower priority list.
27///
28/// ## What kind of config is non-mergeable
29///
30/// The rule of thumb is that if a config is a path of a program,
31/// it should be added to this list.
32const NON_MERGEABLE_LISTS: &[&str] = &[
33    "credential-alias.*",
34    "doc.browser",
35    "host.runner",
36    "registries.*.credential-provider",
37    "registry.credential-provider",
38    "target.*.runner",
39];
40
41/// Similar to [`toml::Value`] but includes the source location where it is defined.
42#[derive(Eq, PartialEq, Clone)]
43pub enum ConfigValue {
44    Integer(i64, Definition),
45    String(String, Definition),
46    List(Vec<ConfigValue>, Definition),
47    Table(HashMap<String, ConfigValue>, Definition),
48    Boolean(bool, Definition),
49}
50
51impl fmt::Debug for ConfigValue {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        match self {
54            CV::Integer(i, def) => write!(f, "{} (from {})", i, def),
55            CV::Boolean(b, def) => write!(f, "{} (from {})", b, def),
56            CV::String(s, def) => write!(f, "{} (from {})", s, def),
57            CV::List(list, def) => {
58                write!(f, "[")?;
59                for (i, item) in list.iter().enumerate() {
60                    if i > 0 {
61                        write!(f, ", ")?;
62                    }
63                    write!(f, "{item:?}")?;
64                }
65                write!(f, "] (from {})", def)
66            }
67            CV::Table(table, _) => write!(f, "{:?}", table),
68        }
69    }
70}
71
72impl ConfigValue {
73    pub(super) fn from_toml(def: Definition, toml: toml::Value) -> CargoResult<ConfigValue> {
74        let mut error_path = Vec::new();
75        Self::from_toml_inner(def, toml, &mut error_path).with_context(|| {
76            let mut it = error_path.iter().rev().peekable();
77            let mut key_path = String::with_capacity(error_path.len() * 3);
78            while let Some(k) = it.next() {
79                match k {
80                    KeyOrIdx::Key(s) => key_path.push_str(&key::escape_key_part(&s)),
81                    KeyOrIdx::Idx(i) => key_path.push_str(&format!("[{i}]")),
82                }
83                if matches!(it.peek(), Some(KeyOrIdx::Key(_))) {
84                    key_path.push('.');
85                }
86            }
87            format!("failed to parse config at `{key_path}`")
88        })
89    }
90
91    fn from_toml_inner(
92        def: Definition,
93        toml: toml::Value,
94        path: &mut Vec<KeyOrIdx>,
95    ) -> CargoResult<ConfigValue> {
96        match toml {
97            toml::Value::String(val) => Ok(CV::String(val, def)),
98            toml::Value::Boolean(b) => Ok(CV::Boolean(b, def)),
99            toml::Value::Integer(i) => Ok(CV::Integer(i, def)),
100            toml::Value::Array(val) => Ok(CV::List(
101                val.into_iter()
102                    .enumerate()
103                    .map(|(i, toml)| {
104                        CV::from_toml_inner(def.clone(), toml, path)
105                            .inspect_err(|_| path.push(KeyOrIdx::Idx(i)))
106                    })
107                    .collect::<CargoResult<_>>()?,
108                def,
109            )),
110            toml::Value::Table(val) => Ok(CV::Table(
111                val.into_iter()
112                    .map(
113                        |(key, value)| match CV::from_toml_inner(def.clone(), value, path) {
114                            Ok(value) => Ok((key, value)),
115                            Err(e) => {
116                                path.push(KeyOrIdx::Key(key));
117                                Err(e)
118                            }
119                        },
120                    )
121                    .collect::<CargoResult<_>>()?,
122                def,
123            )),
124            v => bail!("unsupported TOML configuration type `{}`", v.type_str()),
125        }
126    }
127
128    pub(super) fn into_toml(self) -> toml::Value {
129        match self {
130            CV::Boolean(s, _) => toml::Value::Boolean(s),
131            CV::String(s, _) => toml::Value::String(s),
132            CV::Integer(i, _) => toml::Value::Integer(i),
133            CV::List(l, _) => toml::Value::Array(l.into_iter().map(|cv| cv.into_toml()).collect()),
134            CV::Table(l, _) => {
135                toml::Value::Table(l.into_iter().map(|(k, v)| (k, v.into_toml())).collect())
136            }
137        }
138    }
139
140    /// Merge the given value into self.
141    ///
142    /// If `force` is true, primitive (non-container) types will override existing values
143    /// of equal priority. For arrays, incoming values of equal priority will be placed later.
144    ///
145    /// Container types (tables and arrays) are merged with existing values.
146    ///
147    /// Container and non-container types cannot be mixed.
148    pub(super) fn merge(&mut self, from: ConfigValue, force: bool) -> CargoResult<()> {
149        self.merge_helper(from, force, &mut ConfigKey::new())
150    }
151
152    fn merge_helper(
153        &mut self,
154        from: ConfigValue,
155        force: bool,
156        parts: &mut ConfigKey,
157    ) -> CargoResult<()> {
158        let is_higher_priority = from.definition().is_higher_priority(self.definition());
159        match (self, from) {
160            (&mut CV::List(ref mut old, _), CV::List(ref mut new, _)) => {
161                if is_nonmergeable_list(&parts) {
162                    // Use whichever list is higher priority.
163                    if force || is_higher_priority {
164                        mem::swap(new, old);
165                    }
166                } else {
167                    // Merge the lists together.
168                    if force {
169                        old.append(new);
170                    } else {
171                        new.append(old);
172                        mem::swap(new, old);
173                    }
174                }
175                old.sort_by(|a, b| a.definition().cmp(b.definition()));
176            }
177            (&mut CV::Table(ref mut old, _), CV::Table(ref mut new, _)) => {
178                for (key, value) in mem::take(new) {
179                    match old.entry(key.clone()) {
180                        Entry::Occupied(mut entry) => {
181                            let new_def = value.definition().clone();
182                            let entry = entry.get_mut();
183                            parts.push(&key);
184                            entry.merge_helper(value, force, parts).with_context(|| {
185                                format!(
186                                    "failed to merge key `{}` between \
187                                     {} and {}",
188                                    key,
189                                    entry.definition(),
190                                    new_def,
191                                )
192                            })?;
193                        }
194                        Entry::Vacant(entry) => {
195                            entry.insert(value);
196                        }
197                    };
198                }
199            }
200            // Allow switching types except for tables or arrays.
201            (expected @ &mut CV::List(_, _), found)
202            | (expected @ &mut CV::Table(_, _), found)
203            | (expected, found @ CV::List(_, _))
204            | (expected, found @ CV::Table(_, _)) => {
205                return Err(anyhow!(
206                    "failed to merge config value from `{}` into `{}`: expected {}, but found {}",
207                    found.definition(),
208                    expected.definition(),
209                    expected.desc(),
210                    found.desc()
211                ));
212            }
213            (old, mut new) => {
214                if force || is_higher_priority {
215                    mem::swap(old, &mut new);
216                }
217            }
218        }
219
220        Ok(())
221    }
222
223    pub fn i64(&self, key: &str) -> CargoResult<(i64, &Definition)> {
224        match self {
225            CV::Integer(i, def) => Ok((*i, def)),
226            _ => self.expected("integer", key),
227        }
228    }
229
230    pub fn string(&self, key: &str) -> CargoResult<(&str, &Definition)> {
231        match self {
232            CV::String(s, def) => Ok((s, def)),
233            _ => self.expected("string", key),
234        }
235    }
236
237    pub fn table(&self, key: &str) -> CargoResult<(&HashMap<String, ConfigValue>, &Definition)> {
238        match self {
239            CV::Table(table, def) => Ok((table, def)),
240            _ => self.expected("table", key),
241        }
242    }
243
244    pub fn table_mut(
245        &mut self,
246        key: &str,
247    ) -> CargoResult<(&mut HashMap<String, ConfigValue>, &mut Definition)> {
248        match self {
249            CV::Table(table, def) => Ok((table, def)),
250            _ => self.expected("table", key),
251        }
252    }
253
254    pub fn is_table(&self) -> bool {
255        matches!(self, CV::Table(..))
256    }
257
258    pub fn string_list(&self, key: &str) -> CargoResult<Vec<(String, Definition)>> {
259        match self {
260            CV::List(list, _) => list
261                .iter()
262                .map(|cv| match cv {
263                    CV::String(s, def) => Ok((s.clone(), def.clone())),
264                    _ => self.expected("string", key),
265                })
266                .collect::<CargoResult<_>>(),
267            _ => self.expected("list", key),
268        }
269    }
270
271    pub fn boolean(&self, key: &str) -> CargoResult<(bool, &Definition)> {
272        match self {
273            CV::Boolean(b, def) => Ok((*b, def)),
274            _ => self.expected("bool", key),
275        }
276    }
277
278    pub fn desc(&self) -> &'static str {
279        match *self {
280            CV::Table(..) => "table",
281            CV::List(..) => "array",
282            CV::String(..) => "string",
283            CV::Boolean(..) => "boolean",
284            CV::Integer(..) => "integer",
285        }
286    }
287
288    pub fn definition(&self) -> &Definition {
289        match self {
290            CV::Boolean(_, def)
291            | CV::Integer(_, def)
292            | CV::String(_, def)
293            | CV::List(_, def)
294            | CV::Table(_, def) => def,
295        }
296    }
297
298    pub(super) fn expected<T>(&self, wanted: &str, key: &str) -> CargoResult<T> {
299        bail!(
300            "expected a {}, but found a {} for `{}` in {}",
301            wanted,
302            self.desc(),
303            key,
304            self.definition()
305        )
306    }
307}
308
309pub(super) fn is_nonmergeable_list(key: &ConfigKey) -> bool {
310    NON_MERGEABLE_LISTS.iter().any(|l| key.matches(l))
311}