Skip to content

Commit a8e5bf3

Browse files
authored
refactor: extract ConfigValue to its own module (#16222)
### What does this PR try to resolve? `ConfigValue` is standalone enough to be in its own module (no GlobalContext required) ### How to test and review this PR? Like #16195 but for ConfigValue
2 parents df07b39 + 39dc87b commit a8e5bf3

File tree

3 files changed

+319
-297
lines changed

3 files changed

+319
-297
lines changed
Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
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+
6+
use std::collections::HashMap;
7+
use std::collections::hash_map::Entry;
8+
use std::fmt;
9+
use std::mem;
10+
11+
use anyhow::Context as _;
12+
use anyhow::anyhow;
13+
use anyhow::bail;
14+
15+
use super::ConfigKey;
16+
use super::Definition;
17+
use super::key;
18+
use super::key::KeyOrIdx;
19+
use crate::CargoResult;
20+
21+
use 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.
32+
const 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)]
43+
pub 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+
51+
impl 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+
72+
impl 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+
309+
pub(super) fn is_nonmergeable_list(key: &ConfigKey) -> bool {
310+
NON_MERGEABLE_LISTS.iter().any(|l| key.matches(l))
311+
}

0 commit comments

Comments
 (0)